Compare commits

..

14 Commits

Author SHA1 Message Date
github-actions[bot]
2f92aa90ef 💾 Update Version (#2205)
💾 Sync Versions
> Made via sync_files.yml

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-11-09 23:11:42 +00:00
Anthony Stirling
ba8dd04086 Update build.gradle 2024-11-09 23:11:10 +00:00
github-actions[bot]
c13509cf67 💾 Update Version (#2204)
💾 Sync Versions
> Made via sync_files.yml

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-11-09 23:10:30 +00:00
Anthony Stirling
0ab02e6ceb Update build.gradle 2024-11-09 23:02:12 +00:00
Anthony Stirling
af52652aee Rename release-helm-charts.yml to release-helm-charts.yml-disabled 2024-11-09 23:01:55 +00:00
Anthony Stirling
e534f022f5 Rename lint-helm-charts.yml to lint-helm-charts.yml-disabled 2024-11-09 23:01:26 +00:00
Ludy
84867a7ad7 Fix: Card has no favorite icon (#2203)
fixes the bug if the card has no favorite icon
2024-11-09 15:07:51 +00:00
Renan
e97cb9d49e Add option to insert blank page between pages in Multi-tool (#2194) (#2201) 2024-11-08 22:51:03 +00:00
Anthony Stirling
1b0c1b6cff Searchbar in nav auto select, and exe nolonger disable CLI (#2197)
* fix remmeber me

* remove uselss comment

* Update translation files (#2185)

Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>

* exe no longer disable CLI

---------

Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: a <a>
2024-11-07 21:50:47 +00:00
Rafael Encinas
7eea7fb3cb [Feature] Set Executor Instances limits dynamically from properties (#2193)
* Update 'ProcessExecutor.java' to use dynamic process limits from properties

* Move limits location out of 'application.properties'

* Rename 'SemaphoreLimit' to 'SessionLimit' and bundle with 'Timeout...' into one parent class
2024-11-07 00:43:57 +00:00
github-actions[bot]
c921b5d76f 📝 Update README: Translation Progress Table (#2190)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-11-05 23:28:15 +00:00
Peter Dave Hello
26ec0c5d77 Update and improve zh_TW Traditional Chinese locale (#2188) 2024-11-05 23:26:26 +00:00
ninjat
404e31468e Added input sanitization to fix self-xss issue (#2189) 2024-11-05 21:44:24 +00:00
Anthony Stirling
0c0f61aa0d fix remmeber me (#2184)
* fix remmeber me

* remove uselss comment

* Update translation files (#2185)

Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>

---------

Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-11-05 14:31:31 +00:00
27 changed files with 429 additions and 85 deletions

View File

@@ -210,7 +210,7 @@ Stirling-PDF currently supports 36 languages!
| Spanish (Español) (es_ES) | ![99%](https://geps.dev/progress/99) |
| Swedish (Svenska) (sv_SE) | ![98%](https://geps.dev/progress/98) |
| Thai (ไทย) (th_TH) | ![97%](https://geps.dev/progress/97) |
| Traditional Chinese (繁體中文) (zh_TW) | ![98%](https://geps.dev/progress/98) |
| Traditional Chinese (繁體中文) (zh_TW) | ![99%](https://geps.dev/progress/99) |
| Turkish (Türkçe) (tr_TR) | ![93%](https://geps.dev/progress/93) |
| Ukrainian (Українська) (uk_UA) | ![81%](https://geps.dev/progress/81) |
| Vietnamese (Tiếng Việt) (vi_VN) | ![89%](https://geps.dev/progress/89) |

View File

@@ -22,7 +22,7 @@ ext {
}
group = "stirling.software"
version = "0.31.1"
version = "0.32.0"
java {
// 17 is lowest but we support and recommend 21
@@ -78,7 +78,7 @@ launch4j {
errTitle="Encountered error, Do you have Java 21?"
downloadUrl="https://download.oracle.com/java/21/latest/jdk-21_windows-x64_bin.exe"
variables=["BROWSER_OPEN=true", "ENDPOINTS_GROUPS_TO_REMOVE=CLI"]
variables=["BROWSER_OPEN=true"]
jreMinVersion="17"
mutexName="Stirling-PDF"

View File

@@ -1,5 +1,5 @@
apiVersion: v2
appVersion: 0.31.1
appVersion: 0.32.0
description: locally hosted web application that allows you to perform various operations
on PDF files
home: https://github.com/Stirling-Tools/Stirling-PDF
@@ -13,4 +13,4 @@ maintainers:
name: stirling-pdf-chart
sources:
- https://github.com/Stirling-Tools/Stirling-PDF
version: 1.0.1
version: 1.1.0

View File

@@ -191,6 +191,7 @@ public class EndpointConfiguration {
// LibreOffice
addEndpointToGroup("LibreOffice", "repair");
addEndpointToGroup("LibreOffice", "file-to-pdf");
addEndpointToGroup("Unoconv", "file-to-pdf");
addEndpointToGroup("LibreOffice", "xlsx-to-pdf");
addEndpointToGroup("LibreOffice", "pdf-to-word");
addEndpointToGroup("LibreOffice", "pdf-to-presentation");

View File

@@ -42,6 +42,7 @@ public class ExternalAppDepConfig {
put("ocrmypdf", List.of("OCRmyPDF"));
put("weasyprint", List.of("Weasyprint"));
put("pdftohtml", List.of("Pdftohtml"));
put("unoconv", List.of("Unoconv"));
}
};
@@ -101,6 +102,7 @@ public class ExternalAppDepConfig {
checkDependencyAndDisableGroup("ocrmypdf");
checkDependencyAndDisableGroup("weasyprint");
checkDependencyAndDisableGroup("pdftohtml");
checkDependencyAndDisableGroup("unoconv");
// Special handling for Python/OpenCV dependencies
boolean pythonAvailable = isCommandAvailable("python3") || isCommandAvailable("python");

View File

@@ -156,10 +156,14 @@ public class SecurityConfiguration {
http.rememberMe(
rememberMeConfigurer ->
rememberMeConfigurer // Use the configurator directly
.key("uniqueAndSecret")
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(1209600) // 2 weeks
);
.tokenValiditySeconds(14 * 24 * 60 * 60) // 14 days
.userDetailsService(
userDetailsService) // Your existing UserDetailsService
.useSecureCookie(true) // Enable secure cookie
.rememberMeParameter("remember-me") // Form parameter name
.rememberMeCookieName("remember-me") // Cookie name
.alwaysRemember(false));
http.authorizeHttpRequests(
authz ->
authz.requestMatchers(

View File

@@ -47,6 +47,7 @@ public class ApplicationProperties {
private AutomaticallyGenerated automaticallyGenerated = new AutomaticallyGenerated();
private EnterpriseEdition enterpriseEdition = new EnterpriseEdition();
private AutoPipeline autoPipeline = new AutoPipeline();
private ProcessExecutor processExecutor = new ProcessExecutor();
@Data
public static class AutoPipeline {
@@ -309,4 +310,98 @@ public class ApplicationProperties {
}
}
}
@Data
public static class ProcessExecutor {
private SessionLimit sessionLimit = new SessionLimit();
private TimeoutMinutes timeoutMinutes = new TimeoutMinutes();
@Data
public static class SessionLimit {
private int libreOfficeSessionLimit;
private int pdfToHtmlSessionLimit;
private int ocrMyPdfSessionLimit;
private int pythonOpenCvSessionLimit;
private int ghostScriptSessionLimit;
private int weasyPrintSessionLimit;
private int installAppSessionLimit;
private int calibreSessionLimit;
public int getLibreOfficeSessionLimit() {
return libreOfficeSessionLimit > 0 ? libreOfficeSessionLimit : 1;
}
public int getPdfToHtmlSessionLimit() {
return pdfToHtmlSessionLimit > 0 ? pdfToHtmlSessionLimit : 1;
}
public int getOcrMyPdfSessionLimit() {
return ocrMyPdfSessionLimit > 0 ? ocrMyPdfSessionLimit : 2;
}
public int getPythonOpenCvSessionLimit() {
return pythonOpenCvSessionLimit > 0 ? pythonOpenCvSessionLimit : 8;
}
public int getGhostScriptSessionLimit() {
return ghostScriptSessionLimit > 0 ? ghostScriptSessionLimit : 16;
}
public int getWeasyPrintSessionLimit() {
return weasyPrintSessionLimit > 0 ? weasyPrintSessionLimit : 16;
}
public int getInstallAppSessionLimit() {
return installAppSessionLimit > 0 ? installAppSessionLimit : 1;
}
public int getCalibreSessionLimit() {
return calibreSessionLimit > 0 ? calibreSessionLimit : 1;
}
}
@Data
public static class TimeoutMinutes {
private long libreOfficeTimeoutMinutes;
private long pdfToHtmlTimeoutMinutes;
private long ocrMyPdfTimeoutMinutes;
private long pythonOpenCvTimeoutMinutes;
private long ghostScriptTimeoutMinutes;
private long weasyPrintTimeoutMinutes;
private long installAppTimeoutMinutes;
private long calibreTimeoutMinutes;
public long getLibreOfficeTimeoutMinutes() {
return libreOfficeTimeoutMinutes > 0 ? libreOfficeTimeoutMinutes : 30;
}
public long getPdfToHtmlTimeoutMinutes() {
return pdfToHtmlTimeoutMinutes > 0 ? pdfToHtmlTimeoutMinutes : 20;
}
public long getOcrMyPdfTimeoutMinutes() {
return ocrMyPdfTimeoutMinutes > 0 ? ocrMyPdfTimeoutMinutes : 30;
}
public long getPythonOpenCvTimeoutMinutes() {
return pythonOpenCvTimeoutMinutes > 0 ? pythonOpenCvTimeoutMinutes : 30;
}
public long getGhostScriptTimeoutMinutes() {
return ghostScriptTimeoutMinutes > 0 ? ghostScriptTimeoutMinutes : 30;
}
public long getWeasyPrintTimeoutMinutes() {
return weasyPrintTimeoutMinutes > 0 ? weasyPrintTimeoutMinutes : 30;
}
public long getInstallAppTimeoutMinutes() {
return installAppTimeoutMinutes > 0 ? installAppTimeoutMinutes : 60;
}
public long getCalibreTimeoutMinutes() {
return calibreTimeoutMinutes > 0 ? calibreTimeoutMinutes : 30;
}
}
}
}

View File

@@ -5,6 +5,7 @@ import java.util.Date;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.web.authentication.rememberme.PersistentRememberMeToken;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.transaction.annotation.Transactional;
import stirling.software.SPDF.model.PersistentLogin;
@@ -13,6 +14,7 @@ public class JPATokenRepositoryImpl implements PersistentTokenRepository {
@Autowired private PersistentLoginRepository persistentLoginRepository;
@Override
@Transactional
public void createNewToken(PersistentRememberMeToken token) {
PersistentLogin newToken = new PersistentLogin();
newToken.setSeries(token.getSeries());
@@ -23,6 +25,7 @@ public class JPATokenRepositoryImpl implements PersistentTokenRepository {
}
@Override
@Transactional
public void updateToken(String series, String tokenValue, Date lastUsed) {
PersistentLogin existingToken = persistentLoginRepository.findById(series).orElse(null);
if (existingToken != null) {
@@ -43,11 +46,11 @@ public class JPATokenRepositoryImpl implements PersistentTokenRepository {
}
@Override
@Transactional
public void removeUserTokens(String username) {
for (PersistentLogin token : persistentLoginRepository.findAll()) {
if (token.getUsername().equals(username)) {
persistentLoginRepository.delete(token);
}
try {
persistentLoginRepository.deleteByUsername(username);
} catch (Exception e) {
}
}
}

View File

@@ -6,4 +6,6 @@ import org.springframework.stereotype.Repository;
import stirling.software.SPDF.model.PersistentLogin;
@Repository
public interface PersistentLoginRepository extends JpaRepository<PersistentLogin, String> {}
public interface PersistentLoginRepository extends JpaRepository<PersistentLogin, String> {
void deleteByUsername(String username);
}

View File

@@ -18,10 +18,14 @@ import org.slf4j.LoggerFactory;
import io.github.pixee.security.BoundedLineReader;
import stirling.software.SPDF.model.ApplicationProperties;
public class ProcessExecutor {
private static final Logger logger = LoggerFactory.getLogger(ProcessExecutor.class);
private static ApplicationProperties applicationProperties = new ApplicationProperties();
public enum Processes {
LIBRE_OFFICE,
PDFTOHTML,
@@ -45,26 +49,90 @@ public class ProcessExecutor {
key -> {
int semaphoreLimit =
switch (key) {
case LIBRE_OFFICE -> 1;
case PDFTOHTML -> 1;
case OCR_MY_PDF -> 2;
case PYTHON_OPENCV -> 8;
case GHOSTSCRIPT -> 16;
case WEASYPRINT -> 16;
case INSTALL_APP -> 1;
case CALIBRE -> 1;
case LIBRE_OFFICE ->
applicationProperties
.getProcessExecutor()
.getSessionLimit()
.getLibreOfficeSessionLimit();
case PDFTOHTML ->
applicationProperties
.getProcessExecutor()
.getSessionLimit()
.getPdfToHtmlSessionLimit();
case OCR_MY_PDF ->
applicationProperties
.getProcessExecutor()
.getSessionLimit()
.getOcrMyPdfSessionLimit();
case PYTHON_OPENCV ->
applicationProperties
.getProcessExecutor()
.getSessionLimit()
.getPythonOpenCvSessionLimit();
case GHOSTSCRIPT ->
applicationProperties
.getProcessExecutor()
.getSessionLimit()
.getGhostScriptSessionLimit();
case WEASYPRINT ->
applicationProperties
.getProcessExecutor()
.getSessionLimit()
.getWeasyPrintSessionLimit();
case INSTALL_APP ->
applicationProperties
.getProcessExecutor()
.getSessionLimit()
.getInstallAppSessionLimit();
case CALIBRE ->
applicationProperties
.getProcessExecutor()
.getSessionLimit()
.getCalibreSessionLimit();
};
long timeoutMinutes =
switch (key) {
case LIBRE_OFFICE -> 30;
case PDFTOHTML -> 20;
case OCR_MY_PDF -> 30;
case PYTHON_OPENCV -> 30;
case GHOSTSCRIPT -> 30;
case WEASYPRINT -> 30;
case INSTALL_APP -> 60;
case CALIBRE -> 30;
case LIBRE_OFFICE ->
applicationProperties
.getProcessExecutor()
.getTimeoutMinutes()
.getLibreOfficeTimeoutMinutes();
case PDFTOHTML ->
applicationProperties
.getProcessExecutor()
.getTimeoutMinutes()
.getPdfToHtmlTimeoutMinutes();
case OCR_MY_PDF ->
applicationProperties
.getProcessExecutor()
.getTimeoutMinutes()
.getOcrMyPdfTimeoutMinutes();
case PYTHON_OPENCV ->
applicationProperties
.getProcessExecutor()
.getTimeoutMinutes()
.getPythonOpenCvTimeoutMinutes();
case GHOSTSCRIPT ->
applicationProperties
.getProcessExecutor()
.getTimeoutMinutes()
.getGhostScriptTimeoutMinutes();
case WEASYPRINT ->
applicationProperties
.getProcessExecutor()
.getTimeoutMinutes()
.getWeasyPrintTimeoutMinutes();
case INSTALL_APP ->
applicationProperties
.getProcessExecutor()
.getTimeoutMinutes()
.getInstallAppTimeoutMinutes();
case CALIBRE ->
applicationProperties
.getProcessExecutor()
.getTimeoutMinutes()
.getCalibreTimeoutMinutes();
};
return new ProcessExecutor(semaphoreLimit, liveUpdates, timeoutMinutes);
});

View File

@@ -50,4 +50,4 @@ springdoc.swagger-ui.url=/v1/api-docs
posthog.api.key=phc_fiR65u5j6qmXTYL56MNrLZSWqLaDW74OrZH0Insd2xq
posthog.host=https://eu.i.posthog.com
posthog.host=https://eu.i.posthog.com

View File

@@ -527,7 +527,7 @@ replace-color.selectText.8=Zlutý text na černém pozadí
replace-color.selectText.9=Zelený text na černém pozadí
replace-color.selectText.10=Vyberte barvu textu
replace-color.selectText.11=Vyberte barvu pozadí
replace-color.submit= Nahradit
replace-color.submit=Nahradit

View File

@@ -119,7 +119,7 @@ pipelineOptions.validateButton=Validér
enterpriseEdition.button=Opgrader til Pro
enterpriseEdition.warning=Denne funktion er kun tilgængelig for Pro-brugere.
enterpriseEdition.yamlAdvert=Stirling PDF Pro understøtter YAML-konfigurationsfiler og andre SSO-funktioner.
enterpriseEdition.ssoAdvert= søger du flere funktioner til brugerstyring? Prøv Stirling PDF Pro
enterpriseEdition.ssoAdvert=søger du flere funktioner til brugerstyring? Prøv Stirling PDF Pro
#################

View File

@@ -97,7 +97,7 @@ pipeline.configureButton=Configurar
pipeline.defaultOption=Personalizar
pipeline.submitButton=Submeter
pipeline.help=Pipeline Help
pipeline.scanHelp= Ajuda ao Escaneamento de Pastas
pipeline.scanHelp=Ajuda ao Escaneamento de Pastas
pipeline.deletePrompt=Tem a certeza que quer eliminar o pipeline?
######################
@@ -191,7 +191,7 @@ account.changePassword=Alterar a Senha
account.confirmNewPassword=Confirmar Nova Senha
account.signOut=Sair do Sistema
account.yourApiKey=Sua Chave API
account.syncTitle= sincronizar definições do navegador com a conta
account.syncTitle=sincronizar definições do navegador com a conta
account.settingsCompare=Comparação das Definições:
account.property=Propriedade
account.webBrowserSettings=Configurações do Navegador Web

View File

@@ -359,7 +359,7 @@ PDFToHTML.tags=web content,browser friendly
home.PDFToXML.title=PDF в XML
home.PDFToXML.desc=Преобразование PDF в формат XML
PDFToXML.tags= extraksi data,структурированный контент,interop,преобразование,конвертация
PDFToXML.tags=extraksi data,структурированный контент,interop,преобразование,конвертация
home.ScannerImageSplit.title=Обнаружение/разделение отсканированных фотографий
home.ScannerImageSplit.desc=Разделяет несколько фотографий из фото/PDF

View File

@@ -79,8 +79,8 @@ info=資訊
pro=專業版
page=頁面
pages=頁面
loading=Loading...
addToDoc=Add to Document
loading=載入中...
addToDoc=新增至文件
legal.privacy=隱私權政策
legal.terms=使用條款
@@ -140,7 +140,7 @@ navbar.darkmode=深色模式
navbar.language=語言
navbar.settings=設定
navbar.allTools=工具
navbar.multiTool=多功能工具
navbar.multiTool=複合工具
navbar.sections.organize=整理
navbar.sections.convertTo=轉換為 PDF
navbar.sections.convertFrom=從 PDF 轉換
@@ -246,7 +246,7 @@ database.fileNullOrEmpty=檔案不得為空或空白
database.failedImportFile=匯入檔案失敗
session.expired=您的工作階段已過期。請重新整理頁面並再試一次。
session.refreshPage=Refresh Page
session.refreshPage=重新整理頁面
#############
# HOME-PAGE #
@@ -751,7 +751,7 @@ certSign.showSig=顯示簽章
certSign.reason=原因
certSign.location=位置
certSign.name=名稱
certSign.showLogo=Show Logo
certSign.showLogo=顯示 Logo
certSign.submit=簽章 PDF
@@ -786,9 +786,9 @@ compare.highlightColor.2=標示顏色 2
compare.document.1=文件 1
compare.document.2=文件 2
compare.submit=比較
compare.complex.message=One or both of the provided documents are large files, accuracy of comparison may be reduced
compare.large.file.message=One or Both of the provided documents are too large to process
compare.no.text.message=One or both of the selected PDFs have no text content. Please choose PDFs with text for comparison.
compare.complex.message=選擇的檔案大小太大(其中一個或兩者皆是),可能會影響比較的精確度
compare.large.file.message=選擇的檔案大小超出系統限制(其中一個或兩者皆是),無法處理
compare.no.text.message=選擇的 PDF 檔案未包含文字(其中一個或兩者皆是)。請選擇含有文字的 PDF 進行比較
#BookToPDF
BookToPDF.title=電子書和漫畫轉 PDF
@@ -805,17 +805,17 @@ PDFToBook.submit=轉換
#sign
sign.title=簽章
sign.header= PDF
sign.header= PDF
sign.upload=上傳影像
sign.draw=繪製簽章
sign.text=文字輸入
sign.clear=清除
sign.add=新增
sign.saved=Saved Signatures
sign.save=Save Signature
sign.personalSigs=Personal Signatures
sign.sharedSigs=Shared Signatures
sign.noSavedSigs=No saved signatures found
sign.saved=已儲存的簽章
sign.save=儲存簽章
sign.personalSigs=個人簽章
sign.sharedSigs=共用簽章
sign.noSavedSigs=尚未儲存任何簽章
#repair

View File

@@ -102,3 +102,22 @@ metrics:
AutomaticallyGenerated:
key: example
UUID: example
processExecutor:
sessionLimit: # Process executor instances limits
libreOfficeSessionLimit: 1
pdfToHtmlSessionLimit: 1
ocrMyPdfSessionLimit: 2
pythonOpenCvSessionLimit: 8
ghostScriptSessionLimit: 16
weasyPrintSessionLimit: 16
installAppSessionLimit: 1
calibreSessionLimit: 1
timeoutMinutes: # Process executor timeout in minutes
libreOfficetimeoutMinutes: 30
pdfToHtmltimeoutMinutes: 20
pythonOpenCvtimeoutMinutes: 30
ghostScripttimeoutMinutes: 30
weasyPrinttimeoutMinutes: 30
installApptimeoutMinutes: 60
calibretimeoutMinutes: 30

View File

@@ -126,3 +126,12 @@ html[dir="rtl"] .pdf-actions_container:last-child>.pdf-actions_insert-file-butto
aspect-ratio: 1;
border-radius: 100px;
}
.pdf-actions_insert-file-blank-button {
position: absolute;
top: 75%;
right: 50%;
translate: 0% -50%;
aspect-ratio: 1;
border-radius: 100px;
}

View File

@@ -83,14 +83,16 @@ function syncFavorites() {
cards.forEach(card => {
const isFavorite = localStorage.getItem(card.id) === "favorite";
const starIcon = card.querySelector(".favorite-icon span.material-symbols-rounded");
if (isFavorite) {
starIcon.classList.remove("no-fill");
starIcon.classList.add("fill");
card.classList.add("favorite");
} else {
starIcon.classList.remove("fill");
starIcon.classList.add("no-fill");
card.classList.remove("favorite");
if (starIcon) {
if (isFavorite) {
starIcon.classList.remove("no-fill");
starIcon.classList.add("fill");
card.classList.add("favorite");
} else {
starIcon.classList.remove("fill");
starIcon.classList.add("no-fill");
card.classList.remove("favorite");
}
}
});
updateFavoritesSection();
@@ -260,4 +262,4 @@ document.addEventListener("DOMContentLoaded", function () {
}, 500);
showFavoritesOnly();
});
});

View File

@@ -21,27 +21,55 @@ async function displayFiles(files) {
for (let i = 0; i < files.length; i++) {
const pageCount = await getPDFPageCount(files[i]);
const pageLabel = pageCount === 1 ? pageTranslation : pagesTranslation;
// Create list item
const item = document.createElement("li");
item.className = "list-group-item";
item.innerHTML = `
<div class="d-flex justify-content-between align-items-center w-100">
<div class="filename">${files[i].name}</div>
<div class="page-info">
<span class="page-count">${pageCount} ${pageLabel}</span>
</div>
<div class="arrows d-flex">
<button class="btn btn-secondary move-up"><span>&uarr;</span></button>
<button class="btn btn-secondary move-down"><span>&darr;</span></button>
<button class="btn btn-danger remove-file"><span>&times;</span></button>
</div>
</div>
`;
// Create filename div and set textContent to sanitize
const fileNameDiv = document.createElement("div");
fileNameDiv.className = "filename";
fileNameDiv.textContent = files[i].name;
// Create page info div and set textContent to sanitize
const pageInfoDiv = document.createElement("div");
pageInfoDiv.className = "page-info";
const pageCountSpan = document.createElement("span");
pageCountSpan.className = "page-count";
pageCountSpan.textContent = `${pageCount} ${pageLabel}`;
pageInfoDiv.appendChild(pageCountSpan);
// Create arrows div with buttons
const arrowsDiv = document.createElement("div");
arrowsDiv.className = "arrows d-flex";
const moveUpButton = document.createElement("button");
moveUpButton.className = "btn btn-secondary move-up";
moveUpButton.innerHTML = "<span>&uarr;</span>";
const moveDownButton = document.createElement("button");
moveDownButton.className = "btn btn-secondary move-down";
moveDownButton.innerHTML = "<span>&darr;</span>";
const removeButton = document.createElement("button");
removeButton.className = "btn btn-danger remove-file";
removeButton.innerHTML = "<span>&times;</span>";
arrowsDiv.append(moveUpButton, moveDownButton, removeButton);
// Append elements to item and then to list
const itemContainer = document.createElement("div");
itemContainer.className = "d-flex justify-content-between align-items-center w-100";
itemContainer.append(fileNameDiv, pageInfoDiv, arrowsDiv);
item.appendChild(itemContainer);
list.appendChild(item);
}
attachMoveButtons();
}
async function getPDFPageCount(file) {
const blobUrl = URL.createObjectURL(file);
const pdf = await pdfjsLib.getDocument(blobUrl).promise;

View File

@@ -73,6 +73,11 @@ class PdfActionsManager {
this.addFiles(imgContainer);
}
insertFileBlankButtonCallback(e) {
var imgContainer = this.getPageContainer(e.target);
this.addFiles(imgContainer, true);
}
splitFileButtonCallback(e) {
var imgContainer = this.getPageContainer(e.target);
imgContainer.classList.toggle("split-before");
@@ -89,6 +94,7 @@ class PdfActionsManager {
this.rotateCWButtonCallback = this.rotateCWButtonCallback.bind(this);
this.deletePageButtonCallback = this.deletePageButtonCallback.bind(this);
this.insertFileButtonCallback = this.insertFileButtonCallback.bind(this);
this.insertFileBlankButtonCallback = this.insertFileBlankButtonCallback.bind(this);
this.splitFileButtonCallback = this.splitFileButtonCallback.bind(this);
}
@@ -152,6 +158,12 @@ class PdfActionsManager {
splitFileButton.onclick = this.splitFileButtonCallback;
insertFileButtonContainer.appendChild(splitFileButton);
const insertFileBlankButton = document.createElement("button");
insertFileBlankButton.classList.add("btn", "btn-primary", "pdf-actions_insert-file-blank-button");
insertFileBlankButton.innerHTML = `<span class="material-symbols-rounded">insert_page_break</span>`;
insertFileBlankButton.onclick = this.insertFileBlankButtonCallback;
insertFileButtonContainer.appendChild(insertFileBlankButton);
div.appendChild(insertFileButtonContainer);
// add this button to every element, but only show it on the last one :D

View File

@@ -22,6 +22,7 @@ class PdfContainer {
this.nameAndArchiveFiles = this.nameAndArchiveFiles.bind(this);
this.splitPDF = this.splitPDF.bind(this);
this.splitAll = this.splitAll.bind(this);
this.addFilesBlankAll = this.addFilesBlankAll.bind(this)
this.pdfAdapters = pdfAdapters;
@@ -38,6 +39,7 @@ class PdfContainer {
window.exportPdf = this.exportPdf;
window.rotateAll = this.rotateAll;
window.splitAll = this.splitAll;
window.addFilesBlankAll = this.addFilesBlankAll
const filenameInput = document.getElementById("filename-input");
const downloadBtn = document.getElementById("export-button");
@@ -77,19 +79,25 @@ class PdfContainer {
}
}
addFiles(nextSiblingElement) {
var input = document.createElement("input");
input.type = "file";
input.multiple = true;
input.setAttribute("accept", "application/pdf,image/*");
input.onchange = async (e) => {
const files = e.target.files;
addFiles(nextSiblingElement, blank = false) {
if (blank) {
this.addFilesFromFiles(files, nextSiblingElement);
this.updateFilename(files ? files[0].name : "");
};
this.addFilesBlank(nextSiblingElement);
input.click();
} else {
var input = document.createElement("input");
input.type = "file";
input.multiple = true;
input.setAttribute("accept", "application/pdf,image/*");
input.onchange = async (e) => {
const files = e.target.files;
this.addFilesFromFiles(files, nextSiblingElement);
this.updateFilename(files ? files[0].name : "");
};
input.click();
}
}
async addFilesFromFiles(files, nextSiblingElement) {
@@ -108,6 +116,47 @@ class PdfContainer {
});
}
async addFilesBlank(nextSiblingElement) {
const pdfContent = `
%PDF-1.4
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
endobj
3 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Contents 5 0 R >>
endobj
5 0 obj
<< /Length 44 >>
stream
0 0 0 595 0 842 re
W
n
endstream
endobj
xref
0 6
0000000000 65535 f
0000000010 00000 n
0000000071 00000 n
0000000121 00000 n
0000000205 00000 n
0000000400 00000 n
trailer
<< /Size 6 /Root 1 0 R >>
startxref
278
%%EOF
`;
const blob = new Blob([pdfContent], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const file = new File([blob], "blank_page.pdf", { type: "application/pdf" });
await this.addPdfFile(file, nextSiblingElement);
}
rotateElement(element, deg) {
var lastTransform = element.style.rotate;
if (!lastTransform) {
@@ -224,6 +273,17 @@ class PdfContainer {
}
}
addFilesBlankAll() {
const allPages = this.pagesContainer.querySelectorAll(".page-container");
allPages.forEach((page, index) => {
if (index !== 0) {
this.addFiles(page, true)
}
});
}
splitAll() {
const allPages = this.pagesContainer.querySelectorAll(".page-container");
if (this.pagesContainer.querySelectorAll(".split-before").length > 0) {
@@ -450,7 +510,7 @@ function detectImageType(uint8Array) {
// Check for TIFF signature (little-endian and big-endian)
if ((uint8Array[0] === 73 && uint8Array[1] === 73 && uint8Array[2] === 42 && uint8Array[3] === 0) ||
(uint8Array[0] === 77 && uint8Array[1] === 77 && uint8Array[2] === 0 && uint8Array[3] === 42)) {
(uint8Array[0] === 77 && uint8Array[1] === 77 && uint8Array[2] === 0 && uint8Array[3] === 42)) {
return 'TIFF';
}

View File

@@ -70,3 +70,37 @@ document.querySelector("#navbarSearchInput").addEventListener("input", function
resultsBox.style.width = window.navItemMaxWidth + "px";
});
const searchDropdown = document.getElementById('searchDropdown');
const searchInput = document.getElementById('navbarSearchInput');
const dropdownMenu = searchDropdown.querySelector('.dropdown-menu');
// Handle dropdown shown event
searchDropdown.addEventListener('shown.bs.dropdown', function () {
searchInput.focus();
});
// Handle hover opening
searchDropdown.addEventListener('mouseenter', function () {
const dropdownInstance = new bootstrap.Dropdown(searchDropdown);
dropdownInstance.show();
setTimeout(() => {
searchInput.focus();
}, 100);
});
// Handle mouse leave
searchDropdown.addEventListener('mouseleave', function () {
// Check if current value is empty (including if user typed and then deleted)
if (searchInput.value.trim().length === 0) {
searchInput.blur();
const dropdownInstance = new bootstrap.Dropdown(searchDropdown);
dropdownInstance.hide();
}
});
searchDropdown.addEventListener('hidden.bs.dropdown', function () {
if (searchInput.value.trim().length === 0) {
searchInput.blur();
}
});

View File

@@ -90,8 +90,8 @@
</div>
<div class="form-check m-2 mb-3">
<input type="checkbox" id="remember" value="remember-me">
<label for="remember" th:text="#{login.rememberme}"></label>
<input type="checkbox" name="remember-me" id="remember-me">
<label for="remember-me" th:text="#{login.rememberme}"></label>
</div>
<button class="w-100 btn btn-lg btn-primary" type="submit" th:text="#{login.signin}">Sign in</button>
</form>

View File

@@ -47,6 +47,11 @@
cut
</span>
</button>
<button class="btn btn-secondary enable-on-file" onclick="addFilesBlankAll()" disabled>
<span class="material-symbols-rounded">
insert_page_break
</span>
</button>
<button id="export-button" class="btn btn-primary enable-on-file" onclick="exportPdf()" disabled>
<span class="material-symbols-rounded">
download