Merge pull request #34 from Sf298/addGraphicsToRotatePage
Add graphics to rotate page
This commit is contained in:
commit
4c88c1bc54
3 changed files with 126 additions and 19 deletions
|
@ -37,7 +37,7 @@ public class RotationController {
|
||||||
|
|
||||||
@PostMapping("/rotate-pdf")
|
@PostMapping("/rotate-pdf")
|
||||||
public ResponseEntity<byte[]> rotatePDF(@RequestParam("fileInput") MultipartFile pdfFile,
|
public ResponseEntity<byte[]> rotatePDF(@RequestParam("fileInput") MultipartFile pdfFile,
|
||||||
@RequestParam("angle") String angle) throws IOException {
|
@RequestParam("angle") Integer angle) throws IOException {
|
||||||
|
|
||||||
// Load the PDF document
|
// Load the PDF document
|
||||||
PDDocument document = PDDocument.load(pdfFile.getBytes());
|
PDDocument document = PDDocument.load(pdfFile.getBytes());
|
||||||
|
@ -50,7 +50,7 @@ public class RotationController {
|
||||||
|
|
||||||
while (iterPage.hasNext()) {
|
while (iterPage.hasNext()) {
|
||||||
PDPage page = iterPage.next();
|
PDPage page = iterPage.next();
|
||||||
page.setRotation(Integer.valueOf(angle));
|
page.setRotation(page.getRotation() + angle);
|
||||||
}
|
}
|
||||||
|
|
||||||
return PdfUtils.pdfDocToWebResponse(document, pdfFile.getName() + "_rotated.pdf");
|
return PdfUtils.pdfDocToWebResponse(document, pdfFile.getName() + "_rotated.pdf");
|
||||||
|
|
|
@ -6,6 +6,9 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/pdfjs-dist@3.3.122/build/pdf.min.js"></script>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/pdfjs-dist@3.3.122/web/pdf_viewer.min.css" rel="stylesheet">
|
||||||
|
|
||||||
<link rel="stylesheet" th:href="@{dark-mode.css}" id="dark-mode-styles">
|
<link rel="stylesheet" th:href="@{dark-mode.css}" id="dark-mode-styles">
|
||||||
<script>
|
<script>
|
||||||
function toggleDarkMode() {
|
function toggleDarkMode() {
|
||||||
|
@ -93,4 +96,25 @@
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
</th:block>
|
||||||
|
|
||||||
|
<th:block th:fragment="fileSelector(name, multiple)">
|
||||||
|
<div class="custom-file-chooser">
|
||||||
|
<div class="custom-file">
|
||||||
|
<input type="file" class="custom-file-input" th:name="${name}" th:id="${name}+'-input'" th:multiple="${multiple}">
|
||||||
|
<label class="custom-file-label" th:for="${name}+'-input'" th:text="#{pdfPrompt}"></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script th:inline="javascript">
|
||||||
|
$([[${"#"+name+"-input"}]]).on("change", function() {
|
||||||
|
const files = $(this).get(0).files;
|
||||||
|
const fileNames = Array.from(files).map(f => f.name).join(", ");
|
||||||
|
if (fileNames) {
|
||||||
|
$(this).siblings(".custom-file-label").addClass("selected").html(fileNames);
|
||||||
|
} else {
|
||||||
|
$(this).siblings(".custom-file-label").addClass("selected").html([[#{pdfPrompt}]]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</th:block>
|
</th:block>
|
|
@ -15,27 +15,32 @@
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<h2 th:text="#{rotate.header}"></h2>
|
<h2 th:text="#{rotate.header}"></h2>
|
||||||
|
|
||||||
|
<form action="#" th:action="@{rotate-pdf}" th:object="${rotateForm}" method="post" enctype="multipart/form-data">
|
||||||
|
<div th:replace="fragments/common :: fileSelector(name='fileInput', multiple=false)"></div>
|
||||||
|
<input type="hidden" id="angleInput" name="angle" value="0">
|
||||||
|
|
||||||
|
<div id="editSection" style="display: none">
|
||||||
|
<div class="previewContainer">
|
||||||
|
<img id="pdf-preview"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<form action="#" th:action="@{rotate-pdf}" th:object="${rotateForm}"
|
<div class="buttonContainer">
|
||||||
method="post" enctype="multipart/form-data">
|
<button type="button" class="btn btn-secondary" onclick="rotate(-90)">
|
||||||
<div class="custom-file">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-counterclockwise" viewBox="0 0 16 16">
|
||||||
<input type="file" class="custom-file-input" id="fileInput"
|
<path fill-rule="evenodd" d="M8 3a5 5 0 1 1-4.546 2.914.5.5 0 0 0-.908-.417A6 6 0 1 0 8 2v1z"/>
|
||||||
name="fileInput" required> <label
|
<path d="M8 4.466V.534a.25.25 0 0 0-.41-.192L5.23 2.308a.25.25 0 0 0 0 .384l2.36 1.966A.25.25 0 0 0 8 4.466z"/>
|
||||||
class="custom-file-label" for="fileInput" th:text="#{pdfPrompt}"></label>
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="btn btn-primary" th:text="#{rotate.submit}"></button>
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="rotate(90)">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-clockwise" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z"/>
|
||||||
|
<path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label for="angle" th:text="#{rotate.selectAngle}"></label> <select id="angle" class="form-control" name="angle">
|
|
||||||
<option value="90">90</option>
|
|
||||||
<option value="180">180</option>
|
|
||||||
<option value="270">270</option>
|
|
||||||
<option value="-90">-90</option>
|
|
||||||
<option value="-180">-180</option>
|
|
||||||
<option value="-270">-270</option>
|
|
||||||
</select> <br>
|
|
||||||
<button type="submit" class="btn btn-primary" th:text="#{rotate.submit}"></button>
|
|
||||||
</form>
|
</form>
|
||||||
<th:block th:insert="~{fragments/common :: filelist}"></th:block>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -43,6 +48,84 @@
|
||||||
</div>
|
</div>
|
||||||
<div th:insert="~{fragments/footer.html :: footer}"></div>
|
<div th:insert="~{fragments/footer.html :: footer}"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const angleInput = document.getElementById("angleInput");
|
||||||
|
const fileInput = document.getElementById("fileInput-input");
|
||||||
|
const preview = document.getElementById("pdf-preview");
|
||||||
|
fileInput.addEventListener("change", async function() {
|
||||||
|
console.log("loading pdf");
|
||||||
|
|
||||||
|
document.querySelector("#editSection").style.display = "";
|
||||||
|
|
||||||
|
var url = URL.createObjectURL(fileInput.files[0])
|
||||||
|
|
||||||
|
const pdf = await pdfjsLib.getDocument(url).promise;
|
||||||
|
const page = await pdf.getPage(1);
|
||||||
|
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
|
||||||
|
// set the canvas size to the size of the page
|
||||||
|
if (page.rotate == 90 || page.rotate == 270) {
|
||||||
|
canvas.width = page.view[3];
|
||||||
|
canvas.height = page.view[2];
|
||||||
|
} else {
|
||||||
|
canvas.width = page.view[2];
|
||||||
|
canvas.height = page.view[3];
|
||||||
|
}
|
||||||
|
|
||||||
|
// render the page onto the canvas
|
||||||
|
var renderContext = {
|
||||||
|
canvasContext: canvas.getContext("2d"),
|
||||||
|
viewport: page.getViewport({ scale: 1 })
|
||||||
|
};
|
||||||
|
|
||||||
|
await page.render(renderContext).promise;
|
||||||
|
preview.src = canvas.toDataURL();
|
||||||
|
});
|
||||||
|
|
||||||
|
function rotate(deg) {
|
||||||
|
var lastTransform = preview.style.rotate;
|
||||||
|
if (!lastTransform) {
|
||||||
|
lastTransform = "0";
|
||||||
|
}
|
||||||
|
const lastAngle = parseInt(lastTransform.replace(/[^\d-]/g, ''));
|
||||||
|
const newAngle = lastAngle + deg;
|
||||||
|
|
||||||
|
preview.style.rotate = newAngle + "deg";
|
||||||
|
angleInput.value = newAngle;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
#pdf-preview {
|
||||||
|
margin: 0 auto;
|
||||||
|
display: block;
|
||||||
|
max-width: calc(100% - 30px);
|
||||||
|
max-height: calc(100% - 30px);
|
||||||
|
box-shadow: 0 0 4px rgba(100,100,100,.25);
|
||||||
|
transition: rotate .3s;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
translate: -50% -50%;
|
||||||
|
}
|
||||||
|
.previewContainer {
|
||||||
|
aspect-ratio: 1;
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid rgba(0,0,0,.125);
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
padding: 15px;
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonContainer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
Loading…
Reference in a new issue