diff --git a/src/main/java/stirling/software/SPDF/controller/api/other/AutoRenameController.java b/src/main/java/stirling/software/SPDF/controller/api/other/AutoRenameController.java index bfc7674ec..66fd70a69 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/other/AutoRenameController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/other/AutoRenameController.java @@ -77,7 +77,7 @@ public class AutoRenameController { private static final Logger logger = LoggerFactory.getLogger(AutoRenameController.class); private static final float TITLE_FONT_SIZE_THRESHOLD = 20.0f; - private static final int LINE_LIMIT = 7; + private static final int LINE_LIMIT = 11; @PostMapping(consumes = "multipart/form-data", value = "/auto-rename") @Operation(summary = "Extract header from PDF file", description = "This endpoint accepts a PDF file and attempts to extract its title or header based on heuristics. Input:PDF Output:PDF Type:SISO") @@ -136,12 +136,25 @@ public class AutoRenameController { super.getText(doc); processLine(); // Process the last line - // Sort lines by font size in descending order and get the first one - lineInfos.sort(Comparator.comparing((LineInfo li) -> li.fontSize).reversed()); - String title = lineInfos.isEmpty() ? null : lineInfos.get(0).text; + // Merge lines with same font size + List<LineInfo> mergedLineInfos = new ArrayList<>(); + for (int i = 0; i < lineInfos.size(); i++) { + String mergedText = lineInfos.get(i).text; + float fontSize = lineInfos.get(i).fontSize; + while (i + 1 < lineInfos.size() && lineInfos.get(i + 1).fontSize == fontSize) { + mergedText += " " + lineInfos.get(i + 1).text; + i++; + } + mergedLineInfos.add(new LineInfo(mergedText, fontSize)); + } - return title != null ? title : (useFirstTextAsFallback ? (lineInfos.isEmpty() ? null : lineInfos.get(lineInfos.size() - 1).text) : null); + // Sort lines by font size in descending order and get the first one + mergedLineInfos.sort(Comparator.comparing((LineInfo li) -> li.fontSize).reversed()); + String title = mergedLineInfos.isEmpty() ? null : mergedLineInfos.get(0).text; + + return title != null ? title : (useFirstTextAsFallback ? (mergedLineInfos.isEmpty() ? null : mergedLineInfos.get(mergedLineInfos.size() - 1).text) : null); } + }; String header = reader.getText(document); diff --git a/src/main/java/stirling/software/SPDF/controller/web/OtherWebController.java b/src/main/java/stirling/software/SPDF/controller/web/OtherWebController.java index e3c7a41be..8fa57d08f 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/OtherWebController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/OtherWebController.java @@ -147,4 +147,6 @@ public class OtherWebController { return "other/auto-rename"; } + + } diff --git a/src/main/resources/messages_en_GB.properties b/src/main/resources/messages_en_GB.properties index f28286ba3..15ff69d34 100644 --- a/src/main/resources/messages_en_GB.properties +++ b/src/main/resources/messages_en_GB.properties @@ -35,9 +35,11 @@ navbar.pageOps=Page Operations home.multiTool.title=PDF Multi Tool home.multiTool.desc=Merge, Rotate, Rearrange, and Remove pages +multiTool.tags=Multi Tool,Multi operation,UI,click drag,front end,client side home.merge.title=Merge home.merge.desc=Easily merge multiple PDFs into one. +merge.tags=merge,Page operations,Back end,server side home.split.title=Split home.split.desc=Split PDFs into multiple documents diff --git a/src/main/resources/static/css/home.css b/src/main/resources/static/css/home.css index 94414be95..998278e16 100644 --- a/src/main/resources/static/css/home.css +++ b/src/main/resources/static/css/home.css @@ -1,3 +1,17 @@ +#searchBar { + background-image: url('/images/search.svg'); + background-position: 16px 16px; + background-repeat: no-repeat; + width: 100%; + font-size: 16px; + margin-bottom: 12px; + padding: 12px 20px 12px 40px; + border: 1px solid #ddd; + + +} + + .features-container { display: grid; grid-template-columns: repeat(auto-fill, minmax(21rem, 3fr)); diff --git a/src/main/resources/static/css/navbar.css b/src/main/resources/static/css/navbar.css index 60b612c52..5bb99a5ea 100644 --- a/src/main/resources/static/css/navbar.css +++ b/src/main/resources/static/css/navbar.css @@ -1,3 +1,41 @@ + + +#navbarSearch { + top: 100%; + right: 0; +} + +#searchForm { + width: 200px; /* Adjust this value as needed */ +} + +/* Style the search results to match the navbar */ +#searchResults { + max-height: 200px; /* Adjust this value as needed */ + overflow-y: auto; + width: 100%; +} + +#searchResults .dropdown-item { + display: flex; + align-items: center; + white-space: nowrap; + height: 50px; /* Fixed height */ + overflow: hidden; /* Hide overflow */ +} + +#searchResults .icon { + margin-right: 10px; +} + +#searchResults .icon-text { + display: inline; + overflow: hidden; /* Hide overflow */ + text-overflow: ellipsis; /* Add ellipsis for long text */ +} + + + .main-icon { width: 36px; height: 36px; diff --git a/src/main/resources/static/images/adjust-contrast.svg b/src/main/resources/static/images/adjust-contrast.svg new file mode 100644 index 000000000..fea76d92d --- /dev/null +++ b/src/main/resources/static/images/adjust-contrast.svg @@ -0,0 +1,4 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-palette" viewBox="0 0 16 16"> + <path d="M8 5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zm4 3a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zM5.5 7a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm.5 6a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z"/> + <path d="M16 8c0 3.15-1.866 2.585-3.567 2.07C11.42 9.763 10.465 9.473 10 10c-.603.683-.475 1.819-.351 2.92C9.826 14.495 9.996 16 8 16a8 8 0 1 1 8-8zm-8 7c.611 0 .654-.171.655-.176.078-.146.124-.464.07-1.119-.014-.168-.037-.37-.061-.591-.052-.464-.112-1.005-.118-1.462-.01-.707.083-1.61.704-2.314.369-.417.845-.578 1.272-.618.404-.038.812.026 1.16.104.343.077.702.186 1.025.284l.028.008c.346.105.658.199.953.266.653.148.904.083.991.024C14.717 9.38 15 9.161 15 8a7 7 0 1 0-7 7z"/> +</svg> \ No newline at end of file diff --git a/src/main/resources/static/js/homecard.js b/src/main/resources/static/js/homecard.js index f3d3de35e..fb962a12e 100644 --- a/src/main/resources/static/js/homecard.js +++ b/src/main/resources/static/js/homecard.js @@ -1,3 +1,24 @@ +function filterCards() { + var input = document.getElementById('searchBar'); + var filter = input.value.toUpperCase(); + var cards = document.querySelectorAll('.feature-card'); + + for (var i = 0; i < cards.length; i++) { + var card = cards[i]; + var title = card.querySelector('h5.card-title').innerText; + var text = card.querySelector('p.card-text').innerText; + var tags = card.getAttribute('data-tags'); + var content = title + ' ' + text + ' ' + tags; + + if (content.toUpperCase().indexOf(filter) > -1) { + card.style.display = ""; + } else { + card.style.display = "none"; + } + } +} + + function toggleFavorite(element) { var img = element.querySelector('img'); var card = element.closest('.feature-card'); @@ -13,6 +34,7 @@ function toggleFavorite(element) { } reorderCards(); updateFavoritesDropdown(); + filterCards(); } function reorderCards() { @@ -45,5 +67,7 @@ function initializeCards() { }); reorderCards(); updateFavoritesDropdown(); + filterCards(); } + window.onload = initializeCards; \ No newline at end of file diff --git a/src/main/resources/static/js/search.js b/src/main/resources/static/js/search.js new file mode 100644 index 000000000..3c19ed84e --- /dev/null +++ b/src/main/resources/static/js/search.js @@ -0,0 +1,72 @@ +// Toggle search bar when the search icon is clicked +document.querySelector('#search-icon').addEventListener('click', function(e) { + e.preventDefault(); + var searchBar = document.querySelector('#navbarSearch'); + searchBar.classList.toggle('show'); +}); +window.onload = function() { + var items = document.querySelectorAll('.dropdown-item, .nav-link'); + var dummyContainer = document.createElement('div'); + dummyContainer.style.position = 'absolute'; + dummyContainer.style.visibility = 'hidden'; + dummyContainer.style.whiteSpace = 'nowrap'; // Ensure we measure full width + document.body.appendChild(dummyContainer); + + var maxWidth = 0; + + items.forEach(function(item) { + var clone = item.cloneNode(true); + dummyContainer.appendChild(clone); + var width = clone.offsetWidth; + if (width > maxWidth) { + maxWidth = width; + } + dummyContainer.removeChild(clone); + }); + + document.body.removeChild(dummyContainer); + + // Store max width for later use + window.navItemMaxWidth = maxWidth; +}; + +// Show search results as user types in search box +document.querySelector('#navbarSearchInput').addEventListener('input', function(e) { + var searchText = e.target.value.toLowerCase(); + var items = document.querySelectorAll('.dropdown-item, .nav-link'); + var resultsBox = document.querySelector('#searchResults'); + + // Clear any previous results + resultsBox.innerHTML = ''; + + items.forEach(function(item) { + var titleElement = item.querySelector('.icon-text'); + var iconElement = item.querySelector('.icon'); + var itemHref = item.getAttribute('href'); + if (titleElement && iconElement && itemHref !== '#') { + var title = titleElement.innerText.toLowerCase(); + if (title.indexOf(searchText) !== -1 && !resultsBox.querySelector(`a[href="${item.getAttribute('href')}"]`)) { + var result = document.createElement('a'); + result.href = itemHref; + result.classList.add('dropdown-item'); + + var resultIcon = document.createElement('img'); + resultIcon.src = iconElement.src; + resultIcon.alt = 'icon'; + resultIcon.classList.add('icon'); + result.appendChild(resultIcon); + + var resultText = document.createElement('span'); + resultText.textContent = title; + resultText.classList.add('icon-text'); + result.appendChild(resultText); + + resultsBox.appendChild(result); + } + } + }); + + // Set the width of the search results box to the maximum width + resultsBox.style.width = window.navItemMaxWidth + 'px'; +}); + diff --git a/src/main/resources/templates/fragments/card.html b/src/main/resources/templates/fragments/card.html index faa816a42..48142b033 100644 --- a/src/main/resources/templates/fragments/card.html +++ b/src/main/resources/templates/fragments/card.html @@ -1,4 +1,4 @@ -<div th:fragment="card" class="feature-card" th:id="${id}" th:if="${@endpointConfiguration.isEndpointEnabled(cardLink)}"> +<div th:fragment="card" class="feature-card" th:id="${id}" th:if="${@endpointConfiguration.isEndpointEnabled(cardLink)}" data-tags="${tags}"> <a th:href="${cardLink}"> <div class="d-flex align-items-center"> <!-- Add a flex container to align the SVG and title --> <img th:if="${svgPath}" id="card-icon" class="home-card-icon home-card-icon-colour" th:src="${svgPath}" alt="Icon" width="30" height="30"> diff --git a/src/main/resources/templates/fragments/navbar.html b/src/main/resources/templates/fragments/navbar.html index 9403af422..226141f63 100644 --- a/src/main/resources/templates/fragments/navbar.html +++ b/src/main/resources/templates/fragments/navbar.html @@ -205,6 +205,23 @@ </a> </li> + + <!-- Search Button and Search Bar --> +<li class="nav-item position-relative"> + <a href="#" class="nav-link" id="search-icon"> + <img class="navbar-icon" src="images/search.svg" alt="icon" width="24" height="24"> + </a> + <!-- Search Bar --> + <div class="collapse position-absolute" id="navbarSearch"> + <form class="d-flex p-2 bg-white border" id="searchForm"> + <input class="form-control" type="search" placeholder="Search" aria-label="Search" id="navbarSearchInput"> + </form> + <!-- Search Results --> + <div id="searchResults" class="border p-2 bg-white"></div> + </div> +</li> + + </ul> @@ -212,6 +229,7 @@ </div> <script src="js/favourites.js"></script> + <script src="js/search.js"></script> </nav> <div th:insert="~{fragments/errorBannerPerPage.html :: errorBannerPerPage}"></div> diff --git a/src/main/resources/templates/home.html b/src/main/resources/templates/home.html index 33bf6b427..145b65d15 100644 --- a/src/main/resources/templates/home.html +++ b/src/main/resources/templates/home.html @@ -20,8 +20,13 @@ </div> <br class="d-md-none"> <!-- Features --> - <div class="features-container container"> - + <script src="js/homecard.js"></script> + + <div class=" container"> + <input type="text" id="searchBar" onkeyup="filterCards()" placeholder="Search for features..."> + <div class="features-container "> + + <div th:replace="~{fragments/card :: card(id='pipeline', cardTitle=#{home.pipeline.title}, cardText=#{home.pipeline.desc}, cardLink='pipeline', svgPath='images/pipeline.svg')}"></div> <div th:replace="~{fragments/card :: card(id='multi-tool', cardTitle=#{home.multiTool.title}, cardText=#{home.multiTool.desc}, cardLink='multi-tool', svgPath='images/tools.svg')}"></div> @@ -70,13 +75,13 @@ <div th:replace="~{fragments/card :: card(id='add-page-numbers', cardTitle=#{home.add-page-numbers.title}, cardText=#{home.add-page-numbers.desc}, cardLink='add-page-numbers', svgPath='images/add-page-numbers.svg')}"></div> <div th:replace="~{fragments/card :: card(id='auto-rename', cardTitle=#{home.auto-rename.title}, cardText=#{home.auto-rename.desc}, cardLink='auto-rename', svgPath='images/fonts.svg')}"></div> - + <div th:replace="~{fragments/card :: card(id='adjust-contrast', cardTitle=#{home.adjust-contrast.title}, cardText=#{home.adjust-contrast.desc}, cardLink='adjust-contrast', svgPath='images/adjust-contrast.svg')}"></div> - <script src="js/homecard.js"></script> + </div> - </div> + </div> </div> <div th:insert="~{fragments/footer.html :: footer}"></div> </div> </body> diff --git a/src/main/resources/templates/other/adjust-contrast.html b/src/main/resources/templates/other/adjust-contrast.html index 87496d4f5..8250d6093 100644 --- a/src/main/resources/templates/other/adjust-contrast.html +++ b/src/main/resources/templates/other/adjust-contrast.html @@ -13,15 +13,215 @@ <div class="row justify-content-center"> <div class="col-md-6"> <h2 th:text="#{extractImages.header}"></h2> + <input type="file" id="pdf-file" accept="application/pdf" /> + <canvas id="pdf-canvas"></canvas> - <form id="multiPdfForm" th:action="@{adjust-contrast}" method="post" enctype="multipart/form-data"> - <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div> - <div class="form-group"> - <label for="contrastRange">Contrast</label> - <input name="contrastRange" type="range" class="form-control-range" id="contrastRange" min="-100" max="100" value="0" step="1"> - </div> - <button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{extractImages.submit}"></button> - </form> + <h4>Contrast: <span id="contrast-val">100</span>%</h4> + <input type="range" min="0" max="200" value="100" id="contrast-slider" /> + + <h4>Brightness: <span id="brightness-val">100</span>%</h4> + <input type="range" min="0" max="200" value="100" id="brightness-slider" /> + + <h4>Saturation: <span id="saturation-val">100</span>%</h4> + <input type="range" min="0" max="200" value="100" id="saturation-slider" /> + + <button id="download-button">Download</button> + + <script src="pdfjs/pdf.js"></script> + <script> + var canvas = document.getElementById('pdf-canvas'); + var context = canvas.getContext('2d'); + var originalImageData = null; + + function renderPDFAndSaveOriginalImageData(file) { + var fileReader = new FileReader(); + fileReader.onload = function() { + var data = new Uint8Array(this.result); + pdfjsLib.getDocument({data: data}).promise.then(function(pdf) { + pdf.getPage(1).then(function(page) { + var scale = 1.5; + var viewport = page.getViewport({ scale: scale }); + + canvas.height = viewport.height; + canvas.width = viewport.width; + + var renderContext = { + canvasContext: context, + viewport: viewport + }; + + var renderTask = page.render(renderContext); + renderTask.promise.then(function () { + originalImageData = context.getImageData(0, 0, canvas.width, canvas.height); + }); + }); + }); + }; + fileReader.readAsArrayBuffer(file); + } + + function adjustImageProperties() { + var contrast = parseFloat(document.getElementById('contrast-slider').value); + var brightness = parseFloat(document.getElementById('brightness-slider').value); + var saturation = parseFloat(document.getElementById('saturation-slider').value); + + contrast /= 100; // normalize to range [0, 2] + brightness /= 100; // normalize to range [0, 2] + saturation /= 100; // normalize to range [0, 2] + + if (originalImageData) { + var newImageData = context.createImageData(originalImageData.width, originalImageData.height); + newImageData.data.set(originalImageData.data); + + for(var i=0; i<newImageData.data.length; i+=4) + { + var r = newImageData.data[i]; + var g = newImageData.data[i+1]; + var b = newImageData.data[i+2]; + // Adjust contrast + r = adjustContrastForPixel(r, contrast); + g = adjustContrastForPixel(g, contrast); + b = adjustContrastForPixel(b, contrast); + // Adjust brightness + r = adjustBrightnessForPixel(r, brightness); + g = adjustBrightnessForPixel(g, brightness); + b = adjustBrightnessForPixel(b, brightness); + // Adjust saturation + if(i==100){ + console.log('r',r) + console.log('g',g) + console.log('b',b) + console.log('saturation',saturation) + } + var rgb = adjustSaturationForPixel(r, g, b, saturation); + if(i==100){ + console.log('rgb',rgb) + } + newImageData.data[i] = rgb[0]; + newImageData.data[i+1] = rgb[1]; + newImageData.data[i+2] = rgb[2]; + } + + context.putImageData(newImageData, 0, 0); + } + } + + function rgbToHsl(r, g, b) { + r /= 255, g /= 255, b /= 255; + + var max = Math.max(r, g, b), min = Math.min(r, g, b); + var h, s, l = (max + min) / 2; + + if (max === min) { + h = s = 0; // achromatic + } else { + var d = max - min; + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + + switch (max) { + case r: h = (g - b) / d + (g < b ? 6 : 0); break; + case g: h = (b - r) / d + 2; break; + case b: h = (r - g) / d + 4; break; + } + + h /= 6; + } + + return [h, s, l]; + } + + function hslToRgb(h, s, l) { + var r, g, b; + + if (s === 0) { + r = g = b = l; // achromatic + } else { + var hue2rgb = function hue2rgb(p, q, t) { + if (t < 0) t += 1; + if (t > 1) t -= 1; + if (t < 1 / 6) return p + (q - p) * 6 * t; + if (t < 1 / 2) return q; + if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; + return p; + }; + + var q = l < 0.5 ? l * (1 + s) : l + s - l * s; + var p = 2 * l - q; + + r = hue2rgb(p, q, h + 1 / 3); + g = hue2rgb(p, q, h); + b = hue2rgb(p, q, h - 1 / 3); + } + + return [r * 255, g * 255, b * 255]; + } + + function adjustContrastForPixel(pixel, contrast) { + // Normalize to range [-0.5, 0.5] + var normalized = pixel / 255 - 0.5; + + // Apply contrast + normalized *= contrast; + + // Denormalize back to [0, 255] + return (normalized + 0.5) * 255; + } + function clamp(value, min, max) { + return Math.min(Math.max(value, min), max); + } + + function adjustSaturationForPixel(r, g, b, saturation) { + var hsl = rgbToHsl(r, g, b); + + // Adjust saturation + hsl[1] = clamp(hsl[1] * saturation, 0, 1); + + // Convert back to RGB + var rgb = hslToRgb(hsl[0], hsl[1], hsl[2]); + + // Return adjusted RGB values + return rgb; + } + + function adjustBrightnessForPixel(pixel, brightness) { + return Math.max(0, Math.min(255, pixel * brightness)); + } + + function downloadImage() { + var downloadLink = document.createElement('a'); + downloadLink.href = canvas.toDataURL('image/png'); + downloadLink.download = 'download.png'; + downloadLink.click(); + } + + // Event listeners + document.getElementById('pdf-file').addEventListener('change', function(e) { + if (e.target.files.length > 0) { + renderPDFAndSaveOriginalImageData(e.target.files[0]); + } + }); + + document.getElementById('contrast-slider').addEventListener('input', function() { + document.getElementById('contrast-val').textContent = this.value; + adjustImageProperties(); + }); + + document.getElementById('brightness-slider').addEventListener('input', function() { + document.getElementById('brightness-val').textContent = this.value; + adjustImageProperties(); + }); + + document.getElementById('saturation-slider').addEventListener('input', function() { + document.getElementById('saturation-val').textContent = this.value; + adjustImageProperties(); + }); + + document.getElementById('download-button').addEventListener('click', function() { + downloadImage(); + }); + </script> + + </div> </div> </div>