Search bar and adjust contrast

This commit is contained in:
Anthony Stirling 2023-07-05 22:21:43 +01:00
parent 4e28bf03bd
commit a3c7f5aa46
12 changed files with 411 additions and 19 deletions

View file

@ -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;
return title != null ? title : (useFirstTextAsFallback ? (lineInfos.isEmpty() ? null : lineInfos.get(lineInfos.size() - 1).text) : null);
// 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));
}
// 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);

View file

@ -147,4 +147,6 @@ public class OtherWebController {
return "other/auto-rename";
}
}

View file

@ -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

View file

@ -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));

View file

@ -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;

View file

@ -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>

After

Width:  |  Height:  |  Size: 795 B

View file

@ -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;

View file

@ -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';
});

View file

@ -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">

View file

@ -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>

View file

@ -20,7 +20,12 @@
</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>
@ -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>

View file

@ -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>
<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>
<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>
</div>
</div>
</div>