image cad area size

This commit is contained in:
2026-06-11 09:02:22 +02:00
parent d96b4be9e0
commit 33b627f328
19 changed files with 1660 additions and 0 deletions
@@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class CreateCadAreaJobsTable extends AbstractMigration
{
public function change(): void
{
$table = $this->table('cad_area_jobs');
$table
->addColumn('iduser', 'integer', [
'null' => true,
'signed' => false,
'limit' => 10,
])
->addColumn('original_filename', 'string', [
'limit' => 255,
'null' => false,
])
->addColumn('stored_filename', 'string', [
'limit' => 255,
'null' => false,
])
->addColumn('file_path', 'string', [
'limit' => 500,
'null' => false,
])
->addColumn('file_url', 'string', [
'limit' => 500,
'null' => true,
])
->addColumn('file_size', 'integer', [
'null' => true,
'signed' => false,
])
->addColumn('status', 'enum', [
'values' => [
'uploaded',
'processing',
'completed',
'error',
],
'default' => 'uploaded',
'null' => false,
])
->addColumn('area_mm2', 'decimal', [
'precision' => 18,
'scale' => 6,
'null' => true,
])
->addColumn('area_cm2', 'decimal', [
'precision' => 18,
'scale' => 6,
'null' => true,
])
->addColumn('area_m2', 'decimal', [
'precision' => 18,
'scale' => 9,
'null' => true,
])
->addColumn('scale_detected', 'string', [
'limit' => 50,
'null' => true,
])
->addColumn('confidence', 'string', [
'limit' => 50,
'null' => true,
])
->addColumn('message', 'text', [
'null' => true,
])
->addColumn('python_response', 'text', [
'null' => true,
])
->addColumn('created_at', 'timestamp', [
'default' => 'CURRENT_TIMESTAMP',
'null' => true,
])
->addColumn('updated_at', 'timestamp', [
'default' => 'CURRENT_TIMESTAMP',
'update' => 'CURRENT_TIMESTAMP',
'null' => true,
])
->addIndex(['iduser'], [
'name' => 'idx_cad_area_jobs_iduser',
])
->addIndex(['status'], [
'name' => 'idx_cad_area_jobs_status',
])
->create();
}
}
+655
View File
@@ -0,0 +1,655 @@
<?php include('include/headscript.php'); ?>
<?php
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$iduser = $iduserlogin ?? null;
if ($iduser === null) {
$stmt = $pdo->prepare("
SELECT *
FROM cad_area_jobs
ORDER BY id DESC
");
$stmt->execute();
} else {
$stmt = $pdo->prepare("
SELECT *
FROM cad_area_jobs
WHERE iduser = :iduser
ORDER BY id DESC
");
$stmt->execute([
':iduser' => $iduser
]);
}
$jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
<?php include('cssinclude.php'); ?>
<title>Calcolo Area CAD - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>
<style>
body {
font-size: 1.05rem;
background: #f8fafc;
}
.card {
border-radius: 16px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.back-dashboard {
background-color: #cfe3ff !important;
color: #1f2d3d !important;
border: 1px solid #bcd4f4 !important;
border-radius: 10px;
font-weight: 600;
font-size: 1rem;
padding: 10px 18px;
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.1);
transition: all 0.2s ease-in-out;
}
.back-dashboard:hover {
background-color: #b9d3ff !important;
transform: translateY(-2px);
}
.btn-add {
background-color: #0d6efd;
color: #fff;
border-radius: 8px;
padding: 10px 20px;
font-weight: 500;
transition: all 0.2s ease-in-out;
}
.btn-add:hover {
background-color: #0b5ed7;
color: #fff;
transform: scale(1.02);
}
.table thead {
background-color: #cfe3ff;
color: #1f2d3d;
}
.drop-zone {
border: 2px dashed #9fc5f8;
background: #f4f8ff;
border-radius: 16px;
padding: 38px 20px;
text-align: center;
cursor: pointer;
transition: all 0.2s ease-in-out;
}
.drop-zone:hover,
.drop-zone.dragover {
background: #e8f2ff;
border-color: #0d6efd;
transform: translateY(-2px);
}
.drop-zone-icon {
font-size: 3rem;
margin-bottom: 10px;
}
.file-preview {
border: 1px solid #dbeafe;
background: #ffffff;
border-radius: 12px;
padding: 10px 14px;
margin-bottom: 8px;
display: flex;
justify-content: space-between;
align-items: center;
}
.file-preview small {
color: #64748b;
}
.status-badge {
font-size: 0.85rem;
padding: 6px 10px;
border-radius: 999px;
}
#cadAreaTable {
table-layout: fixed;
width: 100% !important;
}
#cadAreaTable th,
#cadAreaTable td {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: middle;
}
#cadAreaTable th:nth-child(1),
#cadAreaTable td:nth-child(1) {
width: 70px;
}
#cadAreaTable th:nth-child(2),
#cadAreaTable td:nth-child(2) {
width: 300px;
}
#cadAreaTable th:nth-child(3),
#cadAreaTable td:nth-child(3) {
width: 130px;
}
#cadAreaTable th:nth-child(4),
#cadAreaTable td:nth-child(4),
#cadAreaTable th:nth-child(5),
#cadAreaTable td:nth-child(5),
#cadAreaTable th:nth-child(6),
#cadAreaTable td:nth-child(6) {
width: 140px;
}
#cadAreaTable th:nth-child(7),
#cadAreaTable td:nth-child(7) {
width: 150px;
}
#cadAreaTable th:nth-child(8),
#cadAreaTable td:nth-child(8) {
width: 260px;
}
.processing-overlay {
position: fixed;
inset: 0;
background: rgba(15, 23, 42, 0.55);
z-index: 9999;
display: none;
align-items: center;
justify-content: center;
}
.processing-box {
background: white;
border-radius: 18px;
padding: 30px;
min-width: 320px;
text-align: center;
box-shadow: 0 12px 35px rgba(0, 0, 0, 0.25);
}
</style>
</head>
<body>
<div class="wrapper" id="appWrapper">
<?php include('include/navbar.php'); ?>
<?php include('include/topbar.php'); ?>
<div class="page-wrapper">
<div class="page-content">
<div class="card p-3 mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<div>
<h5 class="mb-0">Calcolo Area da PDF CAD</h5>
<small class="text-muted">Upload PDF vettoriali e calcolo automatico della superficie del profilo</small>
</div>
<button type="button" class="btn back-dashboard" onclick="location.href='production_dashboard.php'">
↩️ Torna alla Dashboard
</button>
</div>
<div class="card-body">
<div id="dropZone" class="drop-zone">
<div class="drop-zone-icon">📐</div>
<h5 class="mb-1">Trascina qui uno o più PDF CAD</h5>
<p class="text-muted mb-2">Oppure clicca per selezionare i file</p>
<small class="text-muted">Formati accettati: PDF - massimo 25 MB per file</small>
<input type="file" id="pdfFiles" accept="application/pdf,.pdf" multiple hidden>
</div>
<div id="selectedFiles" class="mt-3"></div>
<div class="text-end mt-3">
<button id="uploadBtn" class="btn btn-add" disabled>
⬆️ Carica PDF
</button>
<button id="processUploadedBtn" class="btn btn-outline-primary ms-2" disabled>
⚙️ Procedi al calcolo
</button>
</div>
</div>
</div>
<div class="card p-3">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Risultati Elaborazione</h5>
<button id="processSelectedBtn" class="btn btn-add">
⚙️ Calcola selezionati
</button>
</div>
<div class="card-body">
<div class="table-responsive">
<table id="cadAreaTable" class="table table-striped align-middle text-center" style="width:100%;">
<thead>
<tr>
<th>
<input type="checkbox" id="selectAll">
</th>
<th>Nome PDF</th>
<th>Stato</th>
<th>Area mm²</th>
<th>Area cm²</th>
<th>Scala</th>
<th>Confidenza</th>
<th>Azioni</th>
</tr>
</thead>
<tbody>
<?php foreach ($jobs as $job): ?>
<?php
$status = $job['status'];
if ($status === 'completed') {
$badge = "<span class='badge bg-success status-badge'>Completato</span>";
} elseif ($status === 'processing') {
$badge = "<span class='badge bg-warning text-dark status-badge'>In lavorazione</span>";
} elseif ($status === 'error') {
$badge = "<span class='badge bg-danger status-badge'>Errore</span>";
} else {
$badge = "<span class='badge bg-secondary status-badge'>Caricato</span>";
}
$areaMm2 = $job['area_mm2'] !== null ? number_format((float)$job['area_mm2'], 3, ',', '.') : '-';
$areaCm2 = $job['area_cm2'] !== null ? number_format((float)$job['area_cm2'], 4, ',', '.') : '-';
$scale = $job['scale_detected'] ?: '-';
$confidence = $job['confidence'] ?: '-';
$fileUrl = htmlspecialchars($job['file_url'] ?? '', ENT_QUOTES, 'UTF-8');
?>
<tr data-id="<?= (int)$job['id']; ?>">
<td>
<?php if (in_array($job['status'], ['uploaded', 'error'], true)): ?>
<input type="checkbox" class="row-check" value="<?= (int)$job['id']; ?>">
<?php endif; ?>
</td>
<td title="<?= htmlspecialchars($job['original_filename'], ENT_QUOTES, 'UTF-8'); ?>">
<?= htmlspecialchars($job['original_filename'], ENT_QUOTES, 'UTF-8'); ?>
</td>
<td><?= $badge; ?></td>
<td class="fw-semibold"><?= $areaMm2; ?></td>
<td class="fw-semibold"><?= $areaCm2; ?></td>
<td><?= htmlspecialchars($scale, ENT_QUOTES, 'UTF-8'); ?></td>
<td><?= htmlspecialchars($confidence, ENT_QUOTES, 'UTF-8'); ?></td>
<td>
<?php if (!empty($job['file_url'])): ?>
<a href="<?= $fileUrl; ?>" target="_blank" class="btn btn-sm btn-outline-dark">
📄 Apri PDF
</a>
<?php endif; ?>
<?php if (in_array($job['status'], ['uploaded', 'error'], true)): ?>
<button class="btn btn-sm btn-outline-primary process-one" data-id="<?= (int)$job['id']; ?>">
⚙️ Calcola
</button>
<?php endif; ?>
<button class="btn btn-sm btn-outline-danger delete-one" data-id="<?= (int)$job['id']; ?>">
🗑️
</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<?php include('include/footer.php'); ?>
</div>
<div class="processing-overlay" id="processingOverlay">
<div class="processing-box">
<div class="spinner-border text-primary mb-3" role="status"></div>
<h5 class="mb-1">Elaborazione in corso</h5>
<p class="text-muted mb-0">Invio dei PDF al servizio di calcolo area...</p>
</div>
</div>
<?php include('jsinclude.php'); ?>
<script>
let selectedFiles = [];
let uploadedIds = [];
const dropZone = document.getElementById('dropZone');
const fileInput = document.getElementById('pdfFiles');
const selectedFilesBox = document.getElementById('selectedFiles');
const uploadBtn = document.getElementById('uploadBtn');
const processUploadedBtn = document.getElementById('processUploadedBtn');
$(document).ready(function() {
$('#cadAreaTable').DataTable({
order: [
[1, 'asc']
],
pageLength: 25,
language: {
url: 'https://cdn.datatables.net/plug-ins/1.13.6/i18n/it-IT.json'
}
});
});
dropZone.addEventListener('click', function() {
fileInput.click();
});
dropZone.addEventListener('dragover', function(e) {
e.preventDefault();
dropZone.classList.add('dragover');
});
dropZone.addEventListener('dragleave', function() {
dropZone.classList.remove('dragover');
});
dropZone.addEventListener('drop', function(e) {
e.preventDefault();
dropZone.classList.remove('dragover');
handleFiles(e.dataTransfer.files);
});
fileInput.addEventListener('change', function(e) {
handleFiles(e.target.files);
});
function handleFiles(files) {
const incomingFiles = Array.from(files);
incomingFiles.forEach(file => {
const isPdf = file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf');
if (!isPdf) {
Swal.fire({
icon: 'warning',
title: 'File non valido',
text: `${file.name} non è un PDF.`
});
return;
}
if (file.size > 25 * 1024 * 1024) {
Swal.fire({
icon: 'warning',
title: 'File troppo grande',
text: `${file.name} supera il limite di 25 MB.`
});
return;
}
selectedFiles.push(file);
});
renderSelectedFiles();
}
function renderSelectedFiles() {
selectedFilesBox.innerHTML = '';
selectedFiles.forEach((file, index) => {
const sizeMb = (file.size / 1024 / 1024).toFixed(2);
const item = document.createElement('div');
item.className = 'file-preview';
item.innerHTML = `
<div>
<strong>📄 ${escapeHtml(file.name)}</strong><br>
<small>${sizeMb} MB</small>
</div>
<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeSelectedFile(${index})">
Rimuovi
</button>
`;
selectedFilesBox.appendChild(item);
});
uploadBtn.disabled = selectedFiles.length === 0;
}
function removeSelectedFile(index) {
selectedFiles.splice(index, 1);
renderSelectedFiles();
}
uploadBtn.addEventListener('click', function() {
if (selectedFiles.length === 0) {
return;
}
const formData = new FormData();
selectedFiles.forEach(file => {
formData.append('pdf_files[]', file);
});
showOverlay();
fetch('cad_area_upload.php', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
hideOverlay();
if (!data.success) {
Swal.fire({
icon: 'error',
title: 'Errore upload',
text: data.message || 'Upload non riuscito'
});
return;
}
uploadedIds = data.ids || [];
selectedFiles = [];
renderSelectedFiles();
processUploadedBtn.disabled = uploadedIds.length === 0;
Swal.fire({
icon: 'success',
title: 'PDF caricati',
text: 'Ora puoi procedere al calcolo area.',
confirmButtonText: 'OK'
}).then(() => {
location.reload();
});
})
.catch(error => {
hideOverlay();
Swal.fire({
icon: 'error',
title: 'Errore',
text: error.message || 'Errore durante upload'
});
});
});
processUploadedBtn.addEventListener('click', function() {
if (uploadedIds.length === 0) {
return;
}
processJobs(uploadedIds);
});
$('#selectAll').on('change', function() {
$('.row-check').prop('checked', this.checked);
});
$('#processSelectedBtn').on('click', function() {
const ids = $('.row-check:checked').map(function() {
return parseInt($(this).val(), 10);
}).get();
if (ids.length === 0) {
Swal.fire({
icon: 'info',
title: 'Nessun PDF selezionato',
text: 'Seleziona almeno un PDF da elaborare.'
});
return;
}
processJobs(ids);
});
$(document).on('click', '.process-one', function() {
const id = parseInt($(this).data('id'), 10);
processJobs([id]);
});
$(document).on('click', '.delete-one', function() {
const id = parseInt($(this).data('id'), 10);
Swal.fire({
icon: 'warning',
title: 'Eliminare il PDF?',
text: 'Il file e il relativo risultato saranno rimossi.',
showCancelButton: true,
confirmButtonText: 'Sì, elimina',
cancelButtonText: 'Annulla'
}).then(result => {
if (!result.isConfirmed) {
return;
}
fetch('cad_area_delete.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
id: id
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
} else {
Swal.fire({
icon: 'error',
title: 'Errore',
text: data.message || 'Eliminazione non riuscita'
});
}
});
});
});
function processJobs(ids) {
showOverlay();
fetch('cad_area_process.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
ids: ids
})
})
.then(response => response.json())
.then(data => {
hideOverlay();
if (!data.success) {
Swal.fire({
icon: 'error',
title: 'Errore elaborazione',
text: data.message || 'Elaborazione non riuscita'
});
return;
}
Swal.fire({
icon: 'success',
title: 'Elaborazione completata',
text: 'I risultati sono stati aggiornati.',
confirmButtonText: 'OK'
}).then(() => {
location.reload();
});
})
.catch(error => {
hideOverlay();
Swal.fire({
icon: 'error',
title: 'Errore',
text: error.message || 'Errore durante elaborazione'
});
});
}
function showOverlay() {
$('#processingOverlay').css('display', 'flex');
}
function hideOverlay() {
$('#processingOverlay').hide();
}
function escapeHtml(text) {
return text.replace(/[&<>"']/g, function(match) {
return ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
})[match];
});
}
</script>
</body>
</html>
+71
View File
@@ -0,0 +1,71 @@
<?php
header('Content-Type: application/json');
require_once(__DIR__ . '/include/headscript.php');
try {
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$iduser = $iduserlogin ?? null;
$input = json_decode(file_get_contents('php://input'), true);
$id = (int)($input['id'] ?? 0);
if ($id <= 0) {
throw new Exception('ID non valido.');
}
if ($iduser === null) {
$stmt = $pdo->prepare("
SELECT *
FROM cad_area_jobs
WHERE id = :id
LIMIT 1
");
$stmt->execute([
':id' => $id
]);
} else {
$stmt = $pdo->prepare("
SELECT *
FROM cad_area_jobs
WHERE id = :id
AND iduser = :iduser
LIMIT 1
");
$stmt->execute([
':id' => $id,
':iduser' => $iduser
]);
}
$job = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$job) {
throw new Exception('Record non trovato.');
}
if (!empty($job['file_path']) && file_exists($job['file_path'])) {
unlink($job['file_path']);
}
$stmtDelete = $pdo->prepare("
DELETE FROM cad_area_jobs
WHERE id = :id
");
$stmtDelete->execute([':id' => $id]);
echo json_encode([
'success' => true
]);
} catch (Throwable $e) {
error_log('CAD area delete error: ' . $e->getMessage());
echo json_encode([
'success' => false,
'message' => $e->getMessage()
]);
}
+282
View File
@@ -0,0 +1,282 @@
<?php
header('Content-Type: application/json');
require_once(__DIR__ . '/include/headscript.php');
try {
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$iduser = $iduserlogin ?? null;
$input = json_decode(file_get_contents('php://input'), true);
if (empty($input['ids']) || !is_array($input['ids'])) {
throw new Exception('Nessun file selezionato.');
}
$ids = array_values(array_filter(array_map('intval', $input['ids'])));
if (empty($ids)) {
throw new Exception('ID non validi.');
}
/*
* Local Python service URL.
* In produzione su cPanel/Keliweb lo sostituiremo con l'URL reale del servizio Python.
*/
$pythonServiceUrl = 'http://127.0.0.1:5055/calculate';
$results = [];
foreach ($ids as $id) {
/*
* Recupero job.
* Uso placeholder ? per evitare errori PDO HY093.
*/
if ($iduser === null || $iduser === '') {
$stmt = $pdo->prepare("
SELECT *
FROM cad_area_jobs
WHERE id = ?
LIMIT 1
");
$stmt->execute([
$id
]);
} else {
$stmt = $pdo->prepare("
SELECT *
FROM cad_area_jobs
WHERE id = ?
AND iduser = ?
LIMIT 1
");
$stmt->execute([
$id,
$iduser
]);
}
$job = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$job) {
$results[] = [
'id' => $id,
'success' => false,
'message' => 'Record non trovato.'
];
continue;
}
if (empty($job['file_path']) || !file_exists($job['file_path'])) {
updateJobError($pdo, $id, 'File PDF non trovato sul server.');
$results[] = [
'id' => $id,
'success' => false,
'message' => 'File PDF non trovato sul server.'
];
continue;
}
$stmtProcessing = $pdo->prepare("
UPDATE cad_area_jobs
SET
status = ?,
message = ?
WHERE id = ?
");
$stmtProcessing->execute([
'processing',
null,
$id
]);
$pythonResponse = callPythonAreaService(
$pythonServiceUrl,
$job['file_path'],
$job['original_filename']
);
if (empty($pythonResponse['success'])) {
updateJobError(
$pdo,
$id,
$pythonResponse['message'] ?? 'Errore servizio Python.',
$pythonResponse
);
$results[] = [
'id' => $id,
'success' => false,
'message' => $pythonResponse['message'] ?? 'Errore servizio Python.',
'python_response' => $pythonResponse
];
continue;
}
$areaMm2 = isset($pythonResponse['area_mm2']) && $pythonResponse['area_mm2'] !== null
? (float)$pythonResponse['area_mm2']
: null;
$areaCm2 = isset($pythonResponse['area_cm2']) && $pythonResponse['area_cm2'] !== null
? (float)$pythonResponse['area_cm2']
: ($areaMm2 !== null ? $areaMm2 / 100 : null);
$areaM2 = isset($pythonResponse['area_m2']) && $pythonResponse['area_m2'] !== null
? (float)$pythonResponse['area_m2']
: ($areaMm2 !== null ? $areaMm2 / 1000000 : null);
$stmtUpdate = $pdo->prepare("
UPDATE cad_area_jobs
SET
status = ?,
area_mm2 = ?,
area_cm2 = ?,
area_m2 = ?,
scale_detected = ?,
confidence = ?,
message = ?,
python_response = ?
WHERE id = ?
");
$stmtUpdate->execute([
'completed',
$areaMm2,
$areaCm2,
$areaM2,
$pythonResponse['scale_detected'] ?? null,
$pythonResponse['confidence'] ?? null,
$pythonResponse['message'] ?? null,
json_encode($pythonResponse, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE),
$id
]);
$results[] = [
'id' => $id,
'success' => true,
'area_mm2' => $areaMm2,
'area_cm2' => $areaCm2,
'area_m2' => $areaM2,
'message' => $pythonResponse['message'] ?? null
];
}
echo json_encode([
'success' => true,
'results' => $results
]);
} catch (Throwable $e) {
error_log('CAD area process error: ' . $e->getMessage());
error_log('File: ' . $e->getFile());
error_log('Line: ' . $e->getLine());
echo json_encode([
'success' => false,
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine()
]);
}
function callPythonAreaService(string $url, string $filePath, string $originalFilename): array
{
if (!class_exists('CURLFile')) {
return [
'success' => false,
'message' => 'CURLFile non disponibile sul server PHP.'
];
}
if (!function_exists('curl_init')) {
return [
'success' => false,
'message' => 'Estensione PHP cURL non disponibile.'
];
}
$curlFile = new CURLFile($filePath, 'application/pdf', $originalFilename);
$postFields = [
'file' => $curlFile,
'mode' => 'pdf_vector',
'scale_ratio' => '1'
];
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $postFields,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 180,
CURLOPT_CONNECTTIMEOUT => 20,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false
]);
$response = curl_exec($ch);
if ($response === false) {
$error = curl_error($ch);
curl_close($ch);
return [
'success' => false,
'message' => 'Errore cURL verso Python: ' . $error
];
}
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$decoded = json_decode($response, true);
if ($httpCode < 200 || $httpCode >= 300) {
return [
'success' => false,
'message' => 'Servizio Python HTTP ' . $httpCode,
'raw_response' => $response
];
}
if (!is_array($decoded)) {
return [
'success' => false,
'message' => 'Risposta Python non valida.',
'raw_response' => $response
];
}
return $decoded;
}
function updateJobError(PDO $pdo, int $id, string $message, ?array $pythonResponse = null): void
{
$stmt = $pdo->prepare("
UPDATE cad_area_jobs
SET
status = ?,
message = ?,
python_response = ?
WHERE id = ?
");
$stmt->execute([
'error',
$message,
$pythonResponse
? json_encode($pythonResponse, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
: null,
$id
]);
}
+106
View File
@@ -0,0 +1,106 @@
<?php
header('Content-Type: application/json');
require_once(__DIR__ . '/include/headscript.php');
try {
$db = DBHandlerSelect::getInstance();
$pdo = $db->getConnection();
$iduser = $iduserlogin ?? null;
$uploadDir = __DIR__ . '/uploads/cad_area/originals/';
$publicBaseUrl = 'uploads/cad_area/originals/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
if (empty($_FILES['pdf_files'])) {
throw new Exception('Nessun file ricevuto.');
}
$files = $_FILES['pdf_files'];
$insertedIds = [];
for ($i = 0; $i < count($files['name']); $i++) {
if ($files['error'][$i] !== UPLOAD_ERR_OK) {
continue;
}
$originalName = $files['name'][$i];
$tmpName = $files['tmp_name'][$i];
$size = (int)$files['size'][$i];
$extension = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
if ($extension !== 'pdf') {
continue;
}
if ($size > 25 * 1024 * 1024) {
continue;
}
$safeBaseName = preg_replace('/[^a-zA-Z0-9_\-]/', '_', pathinfo($originalName, PATHINFO_FILENAME));
$storedName = date('Ymd_His') . '_' . bin2hex(random_bytes(4)) . '_' . $safeBaseName . '.pdf';
$targetPath = $uploadDir . $storedName;
if (!move_uploaded_file($tmpName, $targetPath)) {
continue;
}
$relativeUrl = $publicBaseUrl . $storedName;
$stmt = $pdo->prepare("
INSERT INTO cad_area_jobs
(
iduser,
original_filename,
stored_filename,
file_path,
file_url,
file_size,
status
)
VALUES
(
:iduser,
:original_filename,
:stored_filename,
:file_path,
:file_url,
:file_size,
'uploaded'
)
");
$stmt->execute([
':iduser' => $iduser,
':original_filename' => $originalName,
':stored_filename' => $storedName,
':file_path' => $targetPath,
':file_url' => $relativeUrl,
':file_size' => $size
]);
$insertedIds[] = (int)$pdo->lastInsertId();
}
if (empty($insertedIds)) {
throw new Exception('Nessun PDF valido caricato.');
}
echo json_encode([
'success' => true,
'ids' => $insertedIds
]);
} catch (Throwable $e) {
error_log('CAD area upload error: ' . $e->getMessage());
echo json_encode([
'success' => false,
'message' => $e->getMessage()
]);
}
+76
View File
@@ -0,0 +1,76 @@
from flask import Flask, request, jsonify
from flask_cors import CORS
import traceback
from cad_vector_area import calculate_pdf_vector_area
app = Flask(__name__)
CORS(app)
@app.route("/health", methods=["GET"])
def health():
return jsonify({
"success": True,
"message": "Python CAD Area service is running"
})
@app.route("/calculate", methods=["POST"])
def calculate():
try:
if "file" not in request.files:
return jsonify({
"success": False,
"message": "No PDF file received"
}), 400
uploaded_file = request.files["file"]
if uploaded_file.filename == "":
return jsonify({
"success": False,
"message": "Empty filename"
}), 400
if not uploaded_file.filename.lower().endswith(".pdf"):
return jsonify({
"success": False,
"message": "Only PDF files are allowed"
}), 400
pdf_bytes = uploaded_file.read()
scale_ratio = request.form.get("scale_ratio", "1")
try:
scale_ratio = float(scale_ratio)
if scale_ratio <= 0:
scale_ratio = 1.0
except ValueError:
scale_ratio = 1.0
result = calculate_pdf_vector_area(
pdf_bytes=pdf_bytes,
filename=uploaded_file.filename,
scale_ratio=scale_ratio
)
status_code = 200 if result.get("success") else 422
return jsonify(result), status_code
except Exception as e:
return jsonify({
"success": False,
"message": str(e),
"trace": traceback.format_exc()
}), 500
if __name__ == "__main__":
app.run(
host="127.0.0.1",
port=5055,
debug=True
)
+375
View File
@@ -0,0 +1,375 @@
import fitz
from shapely.geometry import Polygon
from shapely.validation import make_valid
import math
POINT_TO_MM = 25.4 / 72.0
def point_to_tuple(point):
return float(point.x), float(point.y)
def distance(p1, p2):
return math.hypot(p1[0] - p2[0], p1[1] - p2[1])
def rect_to_polygon(rect):
return [
(float(rect.x0), float(rect.y0)),
(float(rect.x1), float(rect.y0)),
(float(rect.x1), float(rect.y1)),
(float(rect.x0), float(rect.y1)),
(float(rect.x0), float(rect.y0)),
]
def cubic_bezier_points(p0, p1, p2, p3, steps=32):
points = []
for i in range(1, steps + 1):
t = i / steps
x = (
(1 - t) ** 3 * p0[0]
+ 3 * (1 - t) ** 2 * t * p1[0]
+ 3 * (1 - t) * t ** 2 * p2[0]
+ t ** 3 * p3[0]
)
y = (
(1 - t) ** 3 * p0[1]
+ 3 * (1 - t) ** 2 * t * p1[1]
+ 3 * (1 - t) * t ** 2 * p2[1]
+ t ** 3 * p3[1]
)
points.append((x, y))
return points
def polygon_area_mm2(points, scale_ratio=1.0):
polygon = Polygon(points)
if not polygon.is_valid:
polygon = make_valid(polygon)
if polygon.is_empty:
return None
area_points2 = abs(float(polygon.area))
area_mm2 = area_points2 * (POINT_TO_MM ** 2)
area_mm2 = area_mm2 / (scale_ratio ** 2)
return area_mm2
def get_bounds_mm(points, scale_ratio=1.0):
polygon = Polygon(points)
bounds = polygon.bounds
x_min, y_min, x_max, y_max = bounds
width_points = x_max - x_min
height_points = y_max - y_min
width_mm = width_points * POINT_TO_MM / scale_ratio
height_mm = height_points * POINT_TO_MM / scale_ratio
return {
"x_min": x_min,
"y_min": y_min,
"x_max": x_max,
"y_max": y_max,
"width_mm": width_mm,
"height_mm": height_mm,
}
def extract_points_from_drawing(drawing):
points = []
source_type = "path"
for item in drawing.get("items", []):
command = item[0]
if command == "l":
p1 = point_to_tuple(item[1])
p2 = point_to_tuple(item[2])
if not points:
points.append(p1)
if distance(points[-1], p1) > 0.01:
points.append(p1)
points.append(p2)
elif command == "re":
rect = item[1]
source_type = "rectangle"
return rect_to_polygon(rect), source_type
elif command == "c":
# PyMuPDF cubic item is normally:
# ("c", start_point, control_1, control_2, end_point)
if len(item) >= 5:
p0 = point_to_tuple(item[1])
p1 = point_to_tuple(item[2])
p2 = point_to_tuple(item[3])
p3 = point_to_tuple(item[4])
if not points:
points.append(p0)
elif distance(points[-1], p0) > 0.01:
points.append(p0)
points.extend(cubic_bezier_points(p0, p1, p2, p3, steps=32))
return points, source_type
def is_closed(points, tolerance_points=1.5):
if len(points) < 4:
return False
return distance(points[0], points[-1]) <= tolerance_points
def is_simple_rectangle(points, source_type):
if source_type == "rectangle":
return True
# Most CAD frames, dimension boxes and table lines become 5-point rectangles.
if len(points) <= 5:
return True
return False
def reject_reason(points, page_rect, source_type, area_mm2, scale_ratio=1.0):
if len(points) < 6:
return "too_few_points"
if not is_closed(points):
return "not_closed"
if is_simple_rectangle(points, source_type):
return "rectangle_or_box"
if area_mm2 is None or area_mm2 <= 0:
return "zero_area"
bounds = get_bounds_mm(points, scale_ratio)
width_mm = bounds["width_mm"]
height_mm = bounds["height_mm"]
if width_mm <= 0 or height_mm <= 0:
return "invalid_bounds"
# Reject thin long rectangles/lines:
# this is exactly what was happening on Zodiac:
# a long frame/table line was selected as area.
min_side = min(width_mm, height_mm)
max_side = max(width_mm, height_mm)
if min_side < 1.0:
return "thin_line_or_stroke"
if max_side / min_side > 80:
return "extreme_aspect_ratio"
# Reject page frames / title blocks.
page_area_mm2 = (page_rect.width * POINT_TO_MM) * (page_rect.height * POINT_TO_MM)
if area_mm2 > page_area_mm2 * 0.05:
return "too_large_page_element"
# Reject text glyphs / arrows / tiny symbols.
if area_mm2 < 20:
return "too_small_detail"
# Reasonable technical-section limits for this first version.
# We can later make these user-configurable.
if width_mm > 250 or height_mm > 250:
return "too_large_for_profile"
return None
def candidate_score(candidate):
"""
Higher score = more plausible rubber/profile section.
This does not guarantee correctness, but avoids obvious false positives.
"""
area = candidate["area_mm2"]
width = candidate["width_mm"]
height = candidate["height_mm"]
min_side = min(width, height)
max_side = max(width, height)
aspect = max_side / min_side if min_side > 0 else 9999
score = 0
# Prefer meaningful areas.
if area >= 50:
score += 20
if area >= 100:
score += 20
if area >= 300:
score += 10
# Penalize strange aspect ratios.
if aspect <= 10:
score += 20
elif aspect <= 25:
score += 5
else:
score -= 20
# Penalize very large bounding boxes.
if width > 120 or height > 120:
score -= 10
return score
def calculate_pdf_vector_area(pdf_bytes, filename="uploaded.pdf", scale_ratio=1.0):
doc = fitz.open(stream=pdf_bytes, filetype="pdf")
if len(doc) == 0:
return {
"success": False,
"message": "PDF has no pages"
}
page = doc[0]
drawings = page.get_drawings()
diagnostics = {
"filename": filename,
"pages": len(doc),
"page_width_points": float(page.rect.width),
"page_height_points": float(page.rect.height),
"drawings_count": len(drawings),
"scale_ratio_used": scale_ratio,
"raw_closed_candidates_count": 0,
"accepted_candidates_count": 0,
"rejected_candidates_count": 0,
}
if len(drawings) == 0:
return {
"success": False,
"message": "No vector drawings found. This PDF may be raster/scanned.",
"confidence": "low",
"diagnostics": diagnostics
}
accepted_candidates = []
rejected_candidates = []
for index, drawing in enumerate(drawings):
points, source_type = extract_points_from_drawing(drawing)
if len(points) < 4:
continue
closed = is_closed(points)
if closed:
diagnostics["raw_closed_candidates_count"] += 1
area_mm2 = None
if closed:
area_mm2 = polygon_area_mm2(points, scale_ratio=scale_ratio)
bounds_data = None
if closed and area_mm2 is not None and area_mm2 > 0:
bounds_data = get_bounds_mm(points, scale_ratio=scale_ratio)
reason = reject_reason(
points=points,
page_rect=page.rect,
source_type=source_type,
area_mm2=area_mm2,
scale_ratio=scale_ratio
)
candidate = {
"drawing_index": index,
"source_type": source_type,
"drawing_type": drawing.get("type"),
"points_count": len(points),
"area_mm2": round(area_mm2, 6),
"area_cm2": round(area_mm2 / 100.0, 6),
"area_m2": round(area_mm2 / 1_000_000.0, 9),
"width_mm": round(bounds_data["width_mm"], 3),
"height_mm": round(bounds_data["height_mm"], 3),
"bounds_points": {
"x_min": bounds_data["x_min"],
"y_min": bounds_data["y_min"],
"x_max": bounds_data["x_max"],
"y_max": bounds_data["y_max"],
},
"fill": drawing.get("fill"),
"color": drawing.get("color"),
}
if reason is None:
candidate["score"] = candidate_score(candidate)
accepted_candidates.append(candidate)
else:
candidate["rejected_reason"] = reason
# Keep only useful rejected diagnostics, not thousands of tiny glyphs.
if len(rejected_candidates) < 80:
rejected_candidates.append(candidate)
diagnostics["accepted_candidates_count"] = len(accepted_candidates)
diagnostics["rejected_candidates_count"] = len(rejected_candidates)
accepted_candidates.sort(key=lambda item: item["score"], reverse=True)
if not accepted_candidates:
return {
"success": False,
"message": (
"No reliable closed profile found. "
"False positives such as rectangles, frames, dimension lines and text were rejected. "
"This PDF probably needs stitched-contour reconstruction."
),
"confidence": "low",
"diagnostics": diagnostics,
"rejected_candidates_preview": rejected_candidates[:30]
}
best = accepted_candidates[0]
area_mm2 = best["area_mm2"]
# In this MVP, even accepted candidates need validation.
# We do not want to present a wrong number as final production data.
confidence = "needs_validation"
return {
"success": True,
"message": (
"Candidate found after rejecting rectangles, frames and thin lines. "
"Validate the selected candidate before using it as final area."
),
"area_mm2": round(area_mm2, 6),
"area_cm2": round(area_mm2 / 100.0, 6),
"area_m2": round(area_mm2 / 1_000_000.0, 9),
"scale_detected": f"{scale_ratio}:1 manual/default",
"confidence": confidence,
"selected_candidate": best,
"diagnostics": diagnostics,
"accepted_candidates_preview": accepted_candidates[:20],
"rejected_candidates_preview": rejected_candidates[:30]
}
Binary file not shown.