image cad area size
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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 ({
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
})[match];
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -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()
|
||||
]);
|
||||
}
|
||||
@@ -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
|
||||
]);
|
||||
}
|
||||
@@ -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()
|
||||
]);
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
)
|
||||
@@ -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.
Reference in New Issue
Block a user