fix: perspektif crop
This commit is contained in:
@@ -19,30 +19,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Modal Crop -->
|
<?php include 'admin/scanner_modal.php'; ?>
|
||||||
<div class="modal fade" id="modalCrop" tabindex="-1" role="dialog" aria-labelledby="modalCropLabel" aria-hidden="true">
|
|
||||||
<div class="modal-dialog modal-xl" role="document">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title" id="modalCropLabel">Potong & Putar Foto KK</h5>
|
|
||||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="img-container" style="max-height: 500px;">
|
|
||||||
<img id="image-to-crop" src="" style="max-width: 100%; display: block;">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" id="btnRotateLeft" title="Putar Kiri"><i class="fas fa-undo"></i></button>
|
|
||||||
<button type="button" class="btn btn-secondary" id="btnRotateRight" title="Putar Kanan"><i class="fas fa-redo"></i></button>
|
|
||||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Batal</button>
|
|
||||||
<button type="button" class="btn btn-primary" id="btnCrop">Potong & Simpan</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label class="col-sm-2 col-form-label">No KK</label>
|
<label class="col-sm-2 col-form-label">No KK</label>
|
||||||
@@ -109,67 +86,24 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
window.addEventListener('load', function() {
|
window.addEventListener('load', function() {
|
||||||
// Cropper Logic
|
// Scanner Logic
|
||||||
var cropper;
|
|
||||||
var image = document.getElementById('image-to-crop');
|
|
||||||
var inputImage = document.getElementById('foto_kk');
|
var inputImage = document.getElementById('foto_kk');
|
||||||
var modal = $('#modalCrop');
|
var preview = document.getElementById('preview_kk');
|
||||||
|
var hiddenInput = document.getElementById('foto_cropped');
|
||||||
|
|
||||||
|
window.handleScannerResult = function(base64) {
|
||||||
|
preview.src = base64;
|
||||||
|
preview.style.display = 'block'; // Make sure it's visible
|
||||||
|
hiddenInput.value = base64;
|
||||||
|
};
|
||||||
|
|
||||||
inputImage.addEventListener('change', function(e) {
|
inputImage.addEventListener('change', function(e) {
|
||||||
var files = e.target.files;
|
var files = e.target.files;
|
||||||
var done = function(url) {
|
|
||||||
inputImage.value = ''; // Clear input
|
|
||||||
image.src = url;
|
|
||||||
modal.modal('show');
|
|
||||||
};
|
|
||||||
var reader;
|
|
||||||
var file;
|
|
||||||
var url;
|
|
||||||
|
|
||||||
if (files && files.length > 0) {
|
if (files && files.length > 0) {
|
||||||
file = files[0];
|
if (window.openScanner) {
|
||||||
if (URL) {
|
window.openScanner(files[0]);
|
||||||
done(URL.createObjectURL(file));
|
|
||||||
} else if (FileReader) {
|
|
||||||
reader = new FileReader();
|
|
||||||
reader.onload = function(e) {
|
|
||||||
done(reader.result);
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
}
|
}
|
||||||
}
|
inputImage.value = '';
|
||||||
});
|
|
||||||
|
|
||||||
modal.on('shown.bs.modal', function() {
|
|
||||||
cropper = new Cropper(image, {
|
|
||||||
aspectRatio: NaN, // Free Ratio for KK
|
|
||||||
viewMode: 1,
|
|
||||||
autoCropArea: 1,
|
|
||||||
});
|
|
||||||
}).on('hidden.bs.modal', function() {
|
|
||||||
cropper.destroy();
|
|
||||||
cropper = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('btnRotateLeft').addEventListener('click', function() {
|
|
||||||
cropper.rotate(-90);
|
|
||||||
});
|
|
||||||
document.getElementById('btnRotateRight').addEventListener('click', function() {
|
|
||||||
cropper.rotate(90);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('btnCrop').addEventListener('click', function() {
|
|
||||||
var canvas;
|
|
||||||
if (cropper) {
|
|
||||||
canvas = cropper.getCroppedCanvas({ width: 1200 }); // Larger for KK
|
|
||||||
var base64 = canvas.toDataURL('image/jpeg');
|
|
||||||
|
|
||||||
var output = document.getElementById('preview_kk');
|
|
||||||
output.src = base64;
|
|
||||||
output.style.display = 'block';
|
|
||||||
|
|
||||||
document.getElementById('foto_cropped').value = base64;
|
|
||||||
modal.modal('hide');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -33,30 +33,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Modal Crop -->
|
<?php include 'admin/scanner_modal.php'; ?>
|
||||||
<div class="modal fade" id="modalCrop" tabindex="-1" role="dialog" aria-labelledby="modalCropLabel" aria-hidden="true">
|
|
||||||
<div class="modal-dialog modal-xl" role="document">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title" id="modalCropLabel">Potong & Putar Foto KK</h5>
|
|
||||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="img-container" style="max-height: 500px;">
|
|
||||||
<img id="image-to-crop" src="" style="max-width: 100%; display: block;">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" id="btnRotateLeft" title="Putar Kiri"><i class="fas fa-undo"></i></button>
|
|
||||||
<button type="button" class="btn btn-secondary" id="btnRotateRight" title="Putar Kanan"><i class="fas fa-redo"></i></button>
|
|
||||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Batal</button>
|
|
||||||
<button type="button" class="btn btn-primary" id="btnCrop">Potong & Simpan</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label class="col-sm-2 col-form-label">No Sistem</label>
|
<label class="col-sm-2 col-form-label">No Sistem</label>
|
||||||
@@ -139,67 +116,23 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
window.addEventListener('load', function() {
|
window.addEventListener('load', function() {
|
||||||
// Cropper Logic
|
// Scanner Logic
|
||||||
var cropper;
|
|
||||||
var image = document.getElementById('image-to-crop');
|
|
||||||
var inputImage = document.getElementById('foto_kk');
|
var inputImage = document.getElementById('foto_kk');
|
||||||
var modal = $('#modalCrop');
|
var preview = document.getElementById('preview_kk');
|
||||||
|
var hiddenInput = document.getElementById('foto_cropped');
|
||||||
|
|
||||||
|
window.handleScannerResult = function(base64) {
|
||||||
|
preview.src = base64;
|
||||||
|
hiddenInput.value = base64;
|
||||||
|
};
|
||||||
|
|
||||||
inputImage.addEventListener('change', function(e) {
|
inputImage.addEventListener('change', function(e) {
|
||||||
var files = e.target.files;
|
var files = e.target.files;
|
||||||
var done = function(url) {
|
|
||||||
inputImage.value = '';
|
|
||||||
image.src = url;
|
|
||||||
modal.modal('show');
|
|
||||||
};
|
|
||||||
var reader;
|
|
||||||
var file;
|
|
||||||
var url;
|
|
||||||
|
|
||||||
if (files && files.length > 0) {
|
if (files && files.length > 0) {
|
||||||
file = files[0];
|
if (window.openScanner) {
|
||||||
if (URL) {
|
window.openScanner(files[0]);
|
||||||
done(URL.createObjectURL(file));
|
|
||||||
} else if (FileReader) {
|
|
||||||
reader = new FileReader();
|
|
||||||
reader.onload = function(e) {
|
|
||||||
done(reader.result);
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
}
|
}
|
||||||
}
|
inputImage.value = '';
|
||||||
});
|
|
||||||
|
|
||||||
modal.on('shown.bs.modal', function() {
|
|
||||||
cropper = new Cropper(image, {
|
|
||||||
aspectRatio: NaN,
|
|
||||||
viewMode: 1,
|
|
||||||
autoCropArea: 1,
|
|
||||||
});
|
|
||||||
}).on('hidden.bs.modal', function() {
|
|
||||||
cropper.destroy();
|
|
||||||
cropper = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('btnRotateLeft').addEventListener('click', function() {
|
|
||||||
cropper.rotate(-90);
|
|
||||||
});
|
|
||||||
document.getElementById('btnRotateRight').addEventListener('click', function() {
|
|
||||||
cropper.rotate(90);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('btnCrop').addEventListener('click', function() {
|
|
||||||
var canvas;
|
|
||||||
if (cropper) {
|
|
||||||
canvas = cropper.getCroppedCanvas({ width: 1200 });
|
|
||||||
var base64 = canvas.toDataURL('image/jpeg');
|
|
||||||
|
|
||||||
var output = document.getElementById('preview_kk');
|
|
||||||
output.src = base64;
|
|
||||||
// output.style.display = 'block'; // Already block in View? Check img tag
|
|
||||||
|
|
||||||
document.getElementById('foto_cropped').value = base64;
|
|
||||||
modal.modal('hide');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -19,30 +19,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Modal Crop -->
|
<?php include 'admin/scanner_modal.php'; ?>
|
||||||
<div class="modal fade" id="modalCrop" tabindex="-1" role="dialog" aria-labelledby="modalCropLabel" aria-hidden="true">
|
|
||||||
<div class="modal-dialog modal-lg" role="document">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title" id="modalCropLabel">Potong & Putar Foto</h5>
|
|
||||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="img-container" style="max-height: 500px;">
|
|
||||||
<img id="image-to-crop" src="" style="max-width: 100%; display: block;">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" id="btnRotateLeft" title="Putar Kiri"><i class="fas fa-undo"></i></button>
|
|
||||||
<button type="button" class="btn btn-secondary" id="btnRotateRight" title="Putar Kanan"><i class="fas fa-redo"></i></button>
|
|
||||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Batal</button>
|
|
||||||
<button type="button" class="btn btn-primary" id="btnCrop">Potong & Simpan</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label class="col-sm-2 col-form-label">NIK</label>
|
<label class="col-sm-2 col-form-label">NIK</label>
|
||||||
@@ -164,72 +141,31 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
window.addEventListener('load', function() {
|
window.addEventListener('load', function() {
|
||||||
// Cropper Logic
|
// Scanner Logic
|
||||||
var cropper;
|
|
||||||
var image = document.getElementById('image-to-crop');
|
|
||||||
var inputImage = document.getElementById('foto_ktp');
|
var inputImage = document.getElementById('foto_ktp');
|
||||||
var modal = $('#modalCrop');
|
var preview = document.getElementById('preview_ktp');
|
||||||
|
var hiddenInput = document.getElementById('foto_cropped');
|
||||||
|
|
||||||
|
// Defines callback for scanner modal
|
||||||
|
window.handleScannerResult = function(base64) {
|
||||||
|
// Show Preview
|
||||||
|
preview.src = base64;
|
||||||
|
preview.style.display = 'block';
|
||||||
|
// Set Hidden Input
|
||||||
|
hiddenInput.value = base64;
|
||||||
|
};
|
||||||
|
|
||||||
inputImage.addEventListener('change', function(e) {
|
inputImage.addEventListener('change', function(e) {
|
||||||
var files = e.target.files;
|
var files = e.target.files;
|
||||||
var done = function(url) {
|
|
||||||
inputImage.value = ''; // Clear input to avoid double submit issues if cancelled
|
|
||||||
image.src = url;
|
|
||||||
modal.modal('show');
|
|
||||||
};
|
|
||||||
var reader;
|
|
||||||
var file;
|
|
||||||
var url;
|
|
||||||
|
|
||||||
if (files && files.length > 0) {
|
if (files && files.length > 0) {
|
||||||
file = files[0];
|
// Open Smart Scanner
|
||||||
if (URL) {
|
if (window.openScanner) {
|
||||||
done(URL.createObjectURL(file));
|
window.openScanner(files[0]);
|
||||||
} else if (FileReader) {
|
} else {
|
||||||
reader = new FileReader();
|
alert("Scanner library not loaded!");
|
||||||
reader.onload = function(e) {
|
|
||||||
done(reader.result);
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
}
|
}
|
||||||
}
|
// Clear input so same file can be selected again if needed
|
||||||
});
|
inputImage.value = '';
|
||||||
|
|
||||||
modal.on('shown.bs.modal', function() {
|
|
||||||
cropper = new Cropper(image, {
|
|
||||||
aspectRatio: 1.58, // KTP Ratio
|
|
||||||
viewMode: 1,
|
|
||||||
autoCropArea: 1,
|
|
||||||
});
|
|
||||||
}).on('hidden.bs.modal', function() {
|
|
||||||
cropper.destroy();
|
|
||||||
cropper = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('btnRotateLeft').addEventListener('click', function() {
|
|
||||||
cropper.rotate(-90);
|
|
||||||
});
|
|
||||||
document.getElementById('btnRotateRight').addEventListener('click', function() {
|
|
||||||
cropper.rotate(90);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('btnCrop').addEventListener('click', function() {
|
|
||||||
var canvas;
|
|
||||||
if (cropper) {
|
|
||||||
canvas = cropper.getCroppedCanvas({
|
|
||||||
width: 1000,
|
|
||||||
});
|
|
||||||
var base64 = canvas.toDataURL('image/jpeg');
|
|
||||||
|
|
||||||
// Show Preview
|
|
||||||
var output = document.getElementById('preview_ktp');
|
|
||||||
output.src = base64;
|
|
||||||
output.style.display = 'block';
|
|
||||||
|
|
||||||
// Set Hidden Input
|
|
||||||
document.getElementById('foto_cropped').value = base64;
|
|
||||||
|
|
||||||
modal.modal('hide');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -41,30 +41,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Modal Crop -->
|
<?php include 'admin/scanner_modal.php'; ?>
|
||||||
<div class="modal fade" id="modalCrop" tabindex="-1" role="dialog" aria-labelledby="modalCropLabel" aria-hidden="true">
|
|
||||||
<div class="modal-dialog modal-lg" role="document">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title" id="modalCropLabel">Potong & Putar Foto</h5>
|
|
||||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="img-container" style="max-height: 500px;">
|
|
||||||
<img id="image-to-crop" src="" style="max-width: 100%; display: block;">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" id="btnRotateLeft" title="Putar Kiri"><i class="fas fa-undo"></i></button>
|
|
||||||
<button type="button" class="btn btn-secondary" id="btnRotateRight" title="Putar Kanan"><i class="fas fa-redo"></i></button>
|
|
||||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Batal</button>
|
|
||||||
<button type="button" class="btn btn-primary" id="btnCrop">Potong & Simpan</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label class="col-sm-2 col-form-label">NIK</label>
|
<label class="col-sm-2 col-form-label">NIK</label>
|
||||||
@@ -215,66 +192,23 @@
|
|||||||
<script>
|
<script>
|
||||||
// Preview Image
|
// Preview Image
|
||||||
window.addEventListener('load', function() {
|
window.addEventListener('load', function() {
|
||||||
// Cropper Logic
|
// Scanner Logic
|
||||||
var cropper;
|
|
||||||
var image = document.getElementById('image-to-crop');
|
|
||||||
var inputImage = document.getElementById('foto_ktp');
|
var inputImage = document.getElementById('foto_ktp');
|
||||||
var modal = $('#modalCrop');
|
var preview = document.getElementById('preview_ktp');
|
||||||
|
var hiddenInput = document.getElementById('foto_cropped');
|
||||||
|
|
||||||
|
window.handleScannerResult = function(base64) {
|
||||||
|
preview.src = base64;
|
||||||
|
hiddenInput.value = base64;
|
||||||
|
};
|
||||||
|
|
||||||
inputImage.addEventListener('change', function(e) {
|
inputImage.addEventListener('change', function(e) {
|
||||||
var files = e.target.files;
|
var files = e.target.files;
|
||||||
var done = function(url) {
|
|
||||||
inputImage.value = ''; // Clear
|
|
||||||
image.src = url;
|
|
||||||
modal.modal('show');
|
|
||||||
};
|
|
||||||
var reader;
|
|
||||||
var file;
|
|
||||||
var url;
|
|
||||||
|
|
||||||
if (files && files.length > 0) {
|
if (files && files.length > 0) {
|
||||||
file = files[0];
|
if (window.openScanner) {
|
||||||
if (URL) {
|
window.openScanner(files[0]);
|
||||||
done(URL.createObjectURL(file));
|
|
||||||
} else if (FileReader) {
|
|
||||||
reader = new FileReader();
|
|
||||||
reader.onload = function(e) {
|
|
||||||
done(reader.result);
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
}
|
}
|
||||||
}
|
inputImage.value = '';
|
||||||
});
|
|
||||||
|
|
||||||
modal.on('shown.bs.modal', function() {
|
|
||||||
cropper = new Cropper(image, {
|
|
||||||
aspectRatio: 1.58,
|
|
||||||
viewMode: 1,
|
|
||||||
autoCropArea: 1,
|
|
||||||
});
|
|
||||||
}).on('hidden.bs.modal', function() {
|
|
||||||
cropper.destroy();
|
|
||||||
cropper = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('btnRotateLeft').addEventListener('click', function() {
|
|
||||||
cropper.rotate(-90);
|
|
||||||
});
|
|
||||||
document.getElementById('btnRotateRight').addEventListener('click', function() {
|
|
||||||
cropper.rotate(90);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('btnCrop').addEventListener('click', function() {
|
|
||||||
var canvas;
|
|
||||||
if (cropper) {
|
|
||||||
canvas = cropper.getCroppedCanvas({ width: 1000 });
|
|
||||||
var base64 = canvas.toDataURL('image/jpeg');
|
|
||||||
|
|
||||||
var output = document.getElementById('preview_ktp');
|
|
||||||
output.src = base64;
|
|
||||||
|
|
||||||
document.getElementById('foto_cropped').value = base64;
|
|
||||||
modal.modal('hide');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
327
admin/scanner_modal.php
Normal file
327
admin/scanner_modal.php
Normal file
@@ -0,0 +1,327 @@
|
|||||||
|
|
||||||
|
<!-- Modal Scanner -->
|
||||||
|
<div class="modal fade" id="modalScanner" tabindex="-1" role="dialog" aria-labelledby="modalScannerLabel" aria-hidden="true" data-backdrop="static">
|
||||||
|
<div class="modal-dialog modal-xl" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="modalScannerLabel"><i class="fas fa-expand"></i> Smart Scanner</h5>
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body text-center bg-dark p-0" style="position: relative; overflow: hidden; height: 80vh;">
|
||||||
|
<!-- Container for Canvases -->
|
||||||
|
<div id="scanner-container" style="position: relative; margin: auto; display: inline-block;">
|
||||||
|
<canvas id="canvas-image" style="position: absolute; left: 0; top: 0; z-index: 1;"></canvas>
|
||||||
|
<canvas id="canvas-overlay" style="position: absolute; left: 0; top: 0; z-index: 2; cursor: crosshair;"></canvas>
|
||||||
|
</div>
|
||||||
|
<div id="scanner-loading" style="position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); color: white; display: none;">
|
||||||
|
<i class="fas fa-spinner fa-spin fa-3x"></i><br>Detecting Document...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer justify-content-between">
|
||||||
|
<div>
|
||||||
|
<button type="button" class="btn btn-secondary" id="btnScanRotateLeft" title="Putar Kiri (-90°)"><i class="fas fa-undo"></i></button>
|
||||||
|
<button type="button" class="btn btn-secondary" id="btnScanRotateRight" title="Putar Kanan (+90°)"><i class="fas fa-redo"></i></button>
|
||||||
|
<button type="button" class="btn btn-warning" id="btnScanReset"><i class="fas fa-sync"></i> Reset Sudut</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">Batal</button>
|
||||||
|
<button type="button" class="btn btn-primary" id="btnScanSave"><i class="fas fa-check"></i> Simpan Hasil Scan</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.addEventListener('load', function() {
|
||||||
|
// Scanner Variables
|
||||||
|
var scannerModal = $('#modalScanner');
|
||||||
|
var canvasImage = document.getElementById('canvas-image');
|
||||||
|
var canvasOverlay = document.getElementById('canvas-overlay');
|
||||||
|
var ctxImg = canvasImage.getContext('2d');
|
||||||
|
var ctxOver = canvasOverlay.getContext('2d');
|
||||||
|
|
||||||
|
var scanner = new jscanify();
|
||||||
|
var originalImg = new Image();
|
||||||
|
|
||||||
|
// Corner Points (tl, tr, bl, br)
|
||||||
|
var corners = [];
|
||||||
|
var activePoint = null;
|
||||||
|
var isDragging = false;
|
||||||
|
|
||||||
|
// Config
|
||||||
|
const POINT_RADIUS = 15;
|
||||||
|
const POINT_COLOR = '#007bff';
|
||||||
|
const LINE_COLOR = '#00ff00';
|
||||||
|
const LINE_WIDTH = 3;
|
||||||
|
|
||||||
|
// --- Public Function to Open Scanner ---
|
||||||
|
// --- Public Function to Open Scanner ---
|
||||||
|
window.openScanner = function(file) {
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
// Reset State
|
||||||
|
corners = [];
|
||||||
|
activePoint = null;
|
||||||
|
isDragging = false;
|
||||||
|
|
||||||
|
// Show Loading
|
||||||
|
$('#scanner-loading').show();
|
||||||
|
|
||||||
|
// Logic: Wait for BOTH Image Load AND Modal Shown
|
||||||
|
var imgLoaded = false;
|
||||||
|
var modalShown = false;
|
||||||
|
|
||||||
|
function checkReady() {
|
||||||
|
if (imgLoaded && modalShown) {
|
||||||
|
initScanner();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Load Image
|
||||||
|
var reader = new FileReader();
|
||||||
|
reader.onload = function(e) {
|
||||||
|
originalImg.onload = function() {
|
||||||
|
imgLoaded = true;
|
||||||
|
checkReady();
|
||||||
|
};
|
||||||
|
originalImg.src = e.target.result;
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
|
||||||
|
// 2. Show Modal & Listen
|
||||||
|
scannerModal.off('shown.bs.modal'); // Remove old listeners
|
||||||
|
scannerModal.on('shown.bs.modal', function() {
|
||||||
|
modalShown = true;
|
||||||
|
checkReady();
|
||||||
|
});
|
||||||
|
|
||||||
|
scannerModal.modal('show');
|
||||||
|
};
|
||||||
|
|
||||||
|
function initScanner() {
|
||||||
|
// Resize Canvas to fit screen but keep aspect ratio
|
||||||
|
var maxWidth = $('#modalScanner .modal-body').width() - 20;
|
||||||
|
var maxHeight = $('#modalScanner .modal-body').height() - 20;
|
||||||
|
|
||||||
|
var scale = Math.min(maxWidth / originalImg.width, maxHeight / originalImg.height);
|
||||||
|
var w = originalImg.width * scale;
|
||||||
|
var h = originalImg.height * scale;
|
||||||
|
|
||||||
|
canvasImage.width = w;
|
||||||
|
canvasImage.height = h;
|
||||||
|
canvasOverlay.width = w;
|
||||||
|
canvasOverlay.height = h;
|
||||||
|
|
||||||
|
// Resize container
|
||||||
|
$('#scanner-container').css({ width: w, height: h, marginTop: '10px' });
|
||||||
|
|
||||||
|
// Draw Image
|
||||||
|
ctxImg.drawImage(originalImg, 0, 0, w, h);
|
||||||
|
|
||||||
|
// Detect Contour using jscanify
|
||||||
|
try {
|
||||||
|
// jscanify expects an image element, we can pass originalImg but we need to map coordinates
|
||||||
|
// Wait, jscanify uses OpenCV which might not be ready.
|
||||||
|
if (typeof cv !== 'undefined' && cv.Mat) {
|
||||||
|
// We need to work on the original image for detection, then scale points
|
||||||
|
var contour = scanner.findPaper(originalImg);
|
||||||
|
// contour returns { topLeftCorner, topRightCorner, bottomLeftCorner, bottomRightCorner } each {x, y}
|
||||||
|
|
||||||
|
if (contour) {
|
||||||
|
corners = [
|
||||||
|
{ x: contour.topLeftCorner.x * scale, y: contour.topLeftCorner.y * scale },
|
||||||
|
{ x: contour.topRightCorner.x * scale, y: contour.topRightCorner.y * scale },
|
||||||
|
{ x: contour.bottomRightCorner.x * scale, y: contour.bottomRightCorner.y * scale }, // Order: tr -> br -> bl ?? No, usually tl, tr, br, bl order for polygon drawing
|
||||||
|
{ x: contour.bottomLeftCorner.x * scale, y: contour.bottomLeftCorner.y * scale }
|
||||||
|
];
|
||||||
|
// Reorder primarily for logic: TL, TR, BR, BL
|
||||||
|
corners = [
|
||||||
|
{ x: contour.topLeftCorner.x * scale, y: contour.topLeftCorner.y * scale },
|
||||||
|
{ x: contour.topRightCorner.x * scale, y: contour.topRightCorner.y * scale },
|
||||||
|
{ x: contour.bottomRightCorner.x * scale, y: contour.bottomRightCorner.y * scale },
|
||||||
|
{ x: contour.bottomLeftCorner.x * scale, y: contour.bottomLeftCorner.y * scale }
|
||||||
|
];
|
||||||
|
|
||||||
|
} else {
|
||||||
|
defaultCorners(w, h);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn("OpenCV not ready yet");
|
||||||
|
defaultCorners(w, h);
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
console.error("Scanner Error:", e);
|
||||||
|
defaultCorners(w, h); // Fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#scanner-loading').hide();
|
||||||
|
drawOverlay();
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultCorners(w, h) {
|
||||||
|
// Default 20% margin
|
||||||
|
var mX = w * 0.1;
|
||||||
|
var mY = h * 0.1;
|
||||||
|
corners = [
|
||||||
|
{ x: mX, y: mY }, // TL
|
||||||
|
{ x: w - mX, y: mY }, // TR
|
||||||
|
{ x: w - mX, y: h - mY }, // BR
|
||||||
|
{ x: mX, y: h - mY } // BL
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawOverlay() {
|
||||||
|
ctxOver.clearRect(0, 0, canvasOverlay.width, canvasOverlay.height);
|
||||||
|
|
||||||
|
if (corners.length < 4) return;
|
||||||
|
|
||||||
|
// Draw Lines
|
||||||
|
ctxOver.beginPath();
|
||||||
|
ctxOver.lineWidth = LINE_WIDTH;
|
||||||
|
ctxOver.strokeStyle = LINE_COLOR;
|
||||||
|
ctxOver.moveTo(corners[0].x, corners[0].y);
|
||||||
|
ctxOver.lineTo(corners[1].x, corners[1].y);
|
||||||
|
ctxOver.lineTo(corners[2].x, corners[2].y);
|
||||||
|
ctxOver.lineTo(corners[3].x, corners[3].y);
|
||||||
|
ctxOver.closePath();
|
||||||
|
ctxOver.stroke();
|
||||||
|
|
||||||
|
// Draw Points
|
||||||
|
ctxOver.fillStyle = POINT_COLOR;
|
||||||
|
corners.forEach(p => {
|
||||||
|
ctxOver.beginPath();
|
||||||
|
ctxOver.arc(p.x, p.y, POINT_RADIUS, 0, Math.PI * 2);
|
||||||
|
ctxOver.fill();
|
||||||
|
ctxOver.strokeStyle = 'white';
|
||||||
|
ctxOver.lineWidth = 2;
|
||||||
|
ctxOver.stroke();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Mouse / Touch Interactivity ---
|
||||||
|
|
||||||
|
function getMousePos(evt) {
|
||||||
|
var rect = canvasOverlay.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
x: (evt.clientX || evt.touches[0].clientX) - rect.left,
|
||||||
|
y: (evt.clientY || evt.touches[0].clientY) - rect.top
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function isInside(pos, point) {
|
||||||
|
var dx = pos.x - point.x;
|
||||||
|
var dy = pos.y - point.y;
|
||||||
|
return dx * dx + dy * dy <= POINT_RADIUS * POINT_RADIUS * 2; // Bigger hit area
|
||||||
|
}
|
||||||
|
|
||||||
|
canvasOverlay.addEventListener('mousedown', function(e) { handleStart(getMousePos(e)); });
|
||||||
|
canvasOverlay.addEventListener('touchstart', function(e) { handleStart(getMousePos(e)); e.preventDefault(); }, {passive: false});
|
||||||
|
|
||||||
|
window.addEventListener('mousemove', function(e) { if(isDragging) handleMove(getMousePos(e)); }); // Window to catch drag out
|
||||||
|
canvasOverlay.addEventListener('touchmove', function(e) { if(isDragging) handleMove(getMousePos(e)); e.preventDefault(); }, {passive: false});
|
||||||
|
|
||||||
|
window.addEventListener('mouseup', function() { handleEnd(); });
|
||||||
|
window.addEventListener('touchend', function() { handleEnd(); });
|
||||||
|
|
||||||
|
function handleStart(pos) {
|
||||||
|
activePoint = null;
|
||||||
|
corners.forEach((p, i) => {
|
||||||
|
if (isInside(pos, p)) {
|
||||||
|
activePoint = i;
|
||||||
|
isDragging = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMove(pos) {
|
||||||
|
if (activePoint !== null) {
|
||||||
|
// Constrain to canvas?? Optional but good
|
||||||
|
corners[activePoint].x = pos.x;
|
||||||
|
corners[activePoint].y = pos.y;
|
||||||
|
drawOverlay();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEnd() {
|
||||||
|
isDragging = false;
|
||||||
|
activePoint = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Rotate Functions ---
|
||||||
|
function rotateImage(degree) {
|
||||||
|
var offCanvas = document.createElement('canvas');
|
||||||
|
var offCtx = offCanvas.getContext('2d');
|
||||||
|
|
||||||
|
// Swap Width/Height for 90 degree rotation
|
||||||
|
offCanvas.width = originalImg.height;
|
||||||
|
offCanvas.height = originalImg.width;
|
||||||
|
|
||||||
|
offCtx.translate(offCanvas.width / 2, offCanvas.height / 2);
|
||||||
|
offCtx.rotate(degree * Math.PI / 180);
|
||||||
|
offCtx.drawImage(originalImg, -originalImg.width / 2, -originalImg.height / 2);
|
||||||
|
|
||||||
|
// Update originalImg
|
||||||
|
var rotatedUrl = offCanvas.toDataURL();
|
||||||
|
originalImg.onload = function() {
|
||||||
|
initScanner(); // Re-init with new image
|
||||||
|
}
|
||||||
|
originalImg.src = rotatedUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#btnScanRotateLeft').click(function() { rotateImage(-90); });
|
||||||
|
$('#btnScanRotateRight').click(function() { rotateImage(90); });
|
||||||
|
|
||||||
|
// --- Reset Button ---
|
||||||
|
$('#btnScanReset').click(function() {
|
||||||
|
initScanner();
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Save / Extract Button ---
|
||||||
|
$('#btnScanSave').click(function() {
|
||||||
|
// Warp Image
|
||||||
|
try {
|
||||||
|
// 1. Get raw points relative to Original Image
|
||||||
|
var scaleX = originalImg.width / canvasImage.width;
|
||||||
|
var scaleY = originalImg.height / canvasImage.height;
|
||||||
|
|
||||||
|
var tl = { x: corners[0].x * scaleX, y: corners[0].y * scaleY };
|
||||||
|
var tr = { x: corners[1].x * scaleX, y: corners[1].y * scaleY };
|
||||||
|
var br = { x: corners[2].x * scaleX, y: corners[2].y * scaleY };
|
||||||
|
var bl = { x: corners[3].x * scaleX, y: corners[3].y * scaleY };
|
||||||
|
|
||||||
|
// 2. Calculate dimensions of the crop area
|
||||||
|
var widthTop = Math.hypot(tr.x - tl.x, tr.y - tl.y);
|
||||||
|
var widthBottom = Math.hypot(br.x - bl.x, br.y - bl.y);
|
||||||
|
var outputWidth = Math.max(widthTop, widthBottom);
|
||||||
|
|
||||||
|
var heightLeft = Math.hypot(bl.x - tl.x, bl.y - tl.y);
|
||||||
|
var heightRight = Math.hypot(br.x - tr.x, br.y - tr.y);
|
||||||
|
var outputHeight = Math.max(heightLeft, heightRight);
|
||||||
|
|
||||||
|
var extractPoints = {
|
||||||
|
topLeftCorner: tl,
|
||||||
|
topRightCorner: tr,
|
||||||
|
bottomRightCorner: br,
|
||||||
|
bottomLeftCorner: bl
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3. Extract with dynamic dimensions
|
||||||
|
var resultCanvas = scanner.extractPaper(originalImg, outputWidth, outputHeight, extractPoints);
|
||||||
|
var base64 = resultCanvas.toDataURL('image/jpeg');
|
||||||
|
|
||||||
|
if (window.handleScannerResult) {
|
||||||
|
window.handleScannerResult(base64);
|
||||||
|
}
|
||||||
|
|
||||||
|
scannerModal.modal('hide');
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
alert("Gagal memproses gambar: " + e.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -705,6 +705,10 @@
|
|||||||
<script src="plugins/select2/js/select2.full.min.js"></script>
|
<script src="plugins/select2/js/select2.full.min.js"></script>
|
||||||
<!-- Cropper.js -->
|
<!-- Cropper.js -->
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.js"></script>
|
||||||
|
<!-- OpenCV.js (Required for jscanify) -->
|
||||||
|
<script src="https://docs.opencv.org/4.7.0/opencv.js" async></script>
|
||||||
|
<!-- jscanify -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/gh/ColonelParrot/jscanify@master/src/jscanify.min.js"></script>
|
||||||
<!-- DataTables -->
|
<!-- DataTables -->
|
||||||
<script src="plugins/datatables/jquery.dataTables.js"></script>
|
<script src="plugins/datatables/jquery.dataTables.js"></script>
|
||||||
<script src="plugins/datatables-bs4/js/dataTables.bootstrap4.js"></script>
|
<script src="plugins/datatables-bs4/js/dataTables.bootstrap4.js"></script>
|
||||||
|
|||||||
Reference in New Issue
Block a user