Compare commits
14 Commits
stirling-p
...
v0.32.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f92aa90ef | ||
|
|
ba8dd04086 | ||
|
|
c13509cf67 | ||
|
|
0ab02e6ceb | ||
|
|
af52652aee | ||
|
|
e534f022f5 | ||
|
|
84867a7ad7 | ||
|
|
e97cb9d49e | ||
|
|
1b0c1b6cff | ||
|
|
7eea7fb3cb | ||
|
|
c921b5d76f | ||
|
|
26ec0c5d77 | ||
|
|
404e31468e | ||
|
|
0c0f61aa0d |
@@ -210,7 +210,7 @@ Stirling-PDF currently supports 36 languages!
|
|||||||
| Spanish (Español) (es_ES) |  |
|
| Spanish (Español) (es_ES) |  |
|
||||||
| Swedish (Svenska) (sv_SE) |  |
|
| Swedish (Svenska) (sv_SE) |  |
|
||||||
| Thai (ไทย) (th_TH) |  |
|
| Thai (ไทย) (th_TH) |  |
|
||||||
| Traditional Chinese (繁體中文) (zh_TW) |  |
|
| Traditional Chinese (繁體中文) (zh_TW) |  |
|
||||||
| Turkish (Türkçe) (tr_TR) |  |
|
| Turkish (Türkçe) (tr_TR) |  |
|
||||||
| Ukrainian (Українська) (uk_UA) |  |
|
| Ukrainian (Українська) (uk_UA) |  |
|
||||||
| Vietnamese (Tiếng Việt) (vi_VN) |  |
|
| Vietnamese (Tiếng Việt) (vi_VN) |  |
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ ext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
group = "stirling.software"
|
group = "stirling.software"
|
||||||
version = "0.31.1"
|
version = "0.32.0"
|
||||||
|
|
||||||
java {
|
java {
|
||||||
// 17 is lowest but we support and recommend 21
|
// 17 is lowest but we support and recommend 21
|
||||||
@@ -78,7 +78,7 @@ launch4j {
|
|||||||
|
|
||||||
errTitle="Encountered error, Do you have Java 21?"
|
errTitle="Encountered error, Do you have Java 21?"
|
||||||
downloadUrl="https://download.oracle.com/java/21/latest/jdk-21_windows-x64_bin.exe"
|
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"
|
jreMinVersion="17"
|
||||||
|
|
||||||
mutexName="Stirling-PDF"
|
mutexName="Stirling-PDF"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
apiVersion: v2
|
apiVersion: v2
|
||||||
appVersion: 0.31.1
|
appVersion: 0.32.0
|
||||||
description: locally hosted web application that allows you to perform various operations
|
description: locally hosted web application that allows you to perform various operations
|
||||||
on PDF files
|
on PDF files
|
||||||
home: https://github.com/Stirling-Tools/Stirling-PDF
|
home: https://github.com/Stirling-Tools/Stirling-PDF
|
||||||
@@ -13,4 +13,4 @@ maintainers:
|
|||||||
name: stirling-pdf-chart
|
name: stirling-pdf-chart
|
||||||
sources:
|
sources:
|
||||||
- https://github.com/Stirling-Tools/Stirling-PDF
|
- https://github.com/Stirling-Tools/Stirling-PDF
|
||||||
version: 1.0.1
|
version: 1.1.0
|
||||||
|
|||||||
@@ -191,6 +191,7 @@ public class EndpointConfiguration {
|
|||||||
// LibreOffice
|
// LibreOffice
|
||||||
addEndpointToGroup("LibreOffice", "repair");
|
addEndpointToGroup("LibreOffice", "repair");
|
||||||
addEndpointToGroup("LibreOffice", "file-to-pdf");
|
addEndpointToGroup("LibreOffice", "file-to-pdf");
|
||||||
|
addEndpointToGroup("Unoconv", "file-to-pdf");
|
||||||
addEndpointToGroup("LibreOffice", "xlsx-to-pdf");
|
addEndpointToGroup("LibreOffice", "xlsx-to-pdf");
|
||||||
addEndpointToGroup("LibreOffice", "pdf-to-word");
|
addEndpointToGroup("LibreOffice", "pdf-to-word");
|
||||||
addEndpointToGroup("LibreOffice", "pdf-to-presentation");
|
addEndpointToGroup("LibreOffice", "pdf-to-presentation");
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ public class ExternalAppDepConfig {
|
|||||||
put("ocrmypdf", List.of("OCRmyPDF"));
|
put("ocrmypdf", List.of("OCRmyPDF"));
|
||||||
put("weasyprint", List.of("Weasyprint"));
|
put("weasyprint", List.of("Weasyprint"));
|
||||||
put("pdftohtml", List.of("Pdftohtml"));
|
put("pdftohtml", List.of("Pdftohtml"));
|
||||||
|
put("unoconv", List.of("Unoconv"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -101,6 +102,7 @@ public class ExternalAppDepConfig {
|
|||||||
checkDependencyAndDisableGroup("ocrmypdf");
|
checkDependencyAndDisableGroup("ocrmypdf");
|
||||||
checkDependencyAndDisableGroup("weasyprint");
|
checkDependencyAndDisableGroup("weasyprint");
|
||||||
checkDependencyAndDisableGroup("pdftohtml");
|
checkDependencyAndDisableGroup("pdftohtml");
|
||||||
|
checkDependencyAndDisableGroup("unoconv");
|
||||||
|
|
||||||
// Special handling for Python/OpenCV dependencies
|
// Special handling for Python/OpenCV dependencies
|
||||||
boolean pythonAvailable = isCommandAvailable("python3") || isCommandAvailable("python");
|
boolean pythonAvailable = isCommandAvailable("python3") || isCommandAvailable("python");
|
||||||
|
|||||||
@@ -156,10 +156,14 @@ public class SecurityConfiguration {
|
|||||||
http.rememberMe(
|
http.rememberMe(
|
||||||
rememberMeConfigurer ->
|
rememberMeConfigurer ->
|
||||||
rememberMeConfigurer // Use the configurator directly
|
rememberMeConfigurer // Use the configurator directly
|
||||||
.key("uniqueAndSecret")
|
|
||||||
.tokenRepository(persistentTokenRepository())
|
.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(
|
http.authorizeHttpRequests(
|
||||||
authz ->
|
authz ->
|
||||||
authz.requestMatchers(
|
authz.requestMatchers(
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ public class ApplicationProperties {
|
|||||||
private AutomaticallyGenerated automaticallyGenerated = new AutomaticallyGenerated();
|
private AutomaticallyGenerated automaticallyGenerated = new AutomaticallyGenerated();
|
||||||
private EnterpriseEdition enterpriseEdition = new EnterpriseEdition();
|
private EnterpriseEdition enterpriseEdition = new EnterpriseEdition();
|
||||||
private AutoPipeline autoPipeline = new AutoPipeline();
|
private AutoPipeline autoPipeline = new AutoPipeline();
|
||||||
|
private ProcessExecutor processExecutor = new ProcessExecutor();
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public static class AutoPipeline {
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import java.util.Date;
|
|||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.security.web.authentication.rememberme.PersistentRememberMeToken;
|
import org.springframework.security.web.authentication.rememberme.PersistentRememberMeToken;
|
||||||
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
|
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.PersistentLogin;
|
import stirling.software.SPDF.model.PersistentLogin;
|
||||||
|
|
||||||
@@ -13,6 +14,7 @@ public class JPATokenRepositoryImpl implements PersistentTokenRepository {
|
|||||||
@Autowired private PersistentLoginRepository persistentLoginRepository;
|
@Autowired private PersistentLoginRepository persistentLoginRepository;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Transactional
|
||||||
public void createNewToken(PersistentRememberMeToken token) {
|
public void createNewToken(PersistentRememberMeToken token) {
|
||||||
PersistentLogin newToken = new PersistentLogin();
|
PersistentLogin newToken = new PersistentLogin();
|
||||||
newToken.setSeries(token.getSeries());
|
newToken.setSeries(token.getSeries());
|
||||||
@@ -23,6 +25,7 @@ public class JPATokenRepositoryImpl implements PersistentTokenRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Transactional
|
||||||
public void updateToken(String series, String tokenValue, Date lastUsed) {
|
public void updateToken(String series, String tokenValue, Date lastUsed) {
|
||||||
PersistentLogin existingToken = persistentLoginRepository.findById(series).orElse(null);
|
PersistentLogin existingToken = persistentLoginRepository.findById(series).orElse(null);
|
||||||
if (existingToken != null) {
|
if (existingToken != null) {
|
||||||
@@ -43,11 +46,11 @@ public class JPATokenRepositoryImpl implements PersistentTokenRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Transactional
|
||||||
public void removeUserTokens(String username) {
|
public void removeUserTokens(String username) {
|
||||||
for (PersistentLogin token : persistentLoginRepository.findAll()) {
|
try {
|
||||||
if (token.getUsername().equals(username)) {
|
persistentLoginRepository.deleteByUsername(username);
|
||||||
persistentLoginRepository.delete(token);
|
} catch (Exception e) {
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,4 +6,6 @@ import org.springframework.stereotype.Repository;
|
|||||||
import stirling.software.SPDF.model.PersistentLogin;
|
import stirling.software.SPDF.model.PersistentLogin;
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
public interface PersistentLoginRepository extends JpaRepository<PersistentLogin, String> {}
|
public interface PersistentLoginRepository extends JpaRepository<PersistentLogin, String> {
|
||||||
|
void deleteByUsername(String username);
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,10 +18,14 @@ import org.slf4j.LoggerFactory;
|
|||||||
|
|
||||||
import io.github.pixee.security.BoundedLineReader;
|
import io.github.pixee.security.BoundedLineReader;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
|
||||||
public class ProcessExecutor {
|
public class ProcessExecutor {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(ProcessExecutor.class);
|
private static final Logger logger = LoggerFactory.getLogger(ProcessExecutor.class);
|
||||||
|
|
||||||
|
private static ApplicationProperties applicationProperties = new ApplicationProperties();
|
||||||
|
|
||||||
public enum Processes {
|
public enum Processes {
|
||||||
LIBRE_OFFICE,
|
LIBRE_OFFICE,
|
||||||
PDFTOHTML,
|
PDFTOHTML,
|
||||||
@@ -45,26 +49,90 @@ public class ProcessExecutor {
|
|||||||
key -> {
|
key -> {
|
||||||
int semaphoreLimit =
|
int semaphoreLimit =
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case LIBRE_OFFICE -> 1;
|
case LIBRE_OFFICE ->
|
||||||
case PDFTOHTML -> 1;
|
applicationProperties
|
||||||
case OCR_MY_PDF -> 2;
|
.getProcessExecutor()
|
||||||
case PYTHON_OPENCV -> 8;
|
.getSessionLimit()
|
||||||
case GHOSTSCRIPT -> 16;
|
.getLibreOfficeSessionLimit();
|
||||||
case WEASYPRINT -> 16;
|
case PDFTOHTML ->
|
||||||
case INSTALL_APP -> 1;
|
applicationProperties
|
||||||
case CALIBRE -> 1;
|
.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 =
|
long timeoutMinutes =
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case LIBRE_OFFICE -> 30;
|
case LIBRE_OFFICE ->
|
||||||
case PDFTOHTML -> 20;
|
applicationProperties
|
||||||
case OCR_MY_PDF -> 30;
|
.getProcessExecutor()
|
||||||
case PYTHON_OPENCV -> 30;
|
.getTimeoutMinutes()
|
||||||
case GHOSTSCRIPT -> 30;
|
.getLibreOfficeTimeoutMinutes();
|
||||||
case WEASYPRINT -> 30;
|
case PDFTOHTML ->
|
||||||
case INSTALL_APP -> 60;
|
applicationProperties
|
||||||
case CALIBRE -> 30;
|
.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);
|
return new ProcessExecutor(semaphoreLimit, liveUpdates, timeoutMinutes);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -79,8 +79,8 @@ info=資訊
|
|||||||
pro=專業版
|
pro=專業版
|
||||||
page=頁面
|
page=頁面
|
||||||
pages=頁面
|
pages=頁面
|
||||||
loading=Loading...
|
loading=載入中...
|
||||||
addToDoc=Add to Document
|
addToDoc=新增至文件
|
||||||
|
|
||||||
legal.privacy=隱私權政策
|
legal.privacy=隱私權政策
|
||||||
legal.terms=使用條款
|
legal.terms=使用條款
|
||||||
@@ -140,7 +140,7 @@ navbar.darkmode=深色模式
|
|||||||
navbar.language=語言
|
navbar.language=語言
|
||||||
navbar.settings=設定
|
navbar.settings=設定
|
||||||
navbar.allTools=工具
|
navbar.allTools=工具
|
||||||
navbar.multiTool=多功能工具
|
navbar.multiTool=複合工具
|
||||||
navbar.sections.organize=整理
|
navbar.sections.organize=整理
|
||||||
navbar.sections.convertTo=轉換為 PDF
|
navbar.sections.convertTo=轉換為 PDF
|
||||||
navbar.sections.convertFrom=從 PDF 轉換
|
navbar.sections.convertFrom=從 PDF 轉換
|
||||||
@@ -246,7 +246,7 @@ database.fileNullOrEmpty=檔案不得為空或空白
|
|||||||
database.failedImportFile=匯入檔案失敗
|
database.failedImportFile=匯入檔案失敗
|
||||||
|
|
||||||
session.expired=您的工作階段已過期。請重新整理頁面並再試一次。
|
session.expired=您的工作階段已過期。請重新整理頁面並再試一次。
|
||||||
session.refreshPage=Refresh Page
|
session.refreshPage=重新整理頁面
|
||||||
|
|
||||||
#############
|
#############
|
||||||
# HOME-PAGE #
|
# HOME-PAGE #
|
||||||
@@ -751,7 +751,7 @@ certSign.showSig=顯示簽章
|
|||||||
certSign.reason=原因
|
certSign.reason=原因
|
||||||
certSign.location=位置
|
certSign.location=位置
|
||||||
certSign.name=名稱
|
certSign.name=名稱
|
||||||
certSign.showLogo=Show Logo
|
certSign.showLogo=顯示 Logo
|
||||||
certSign.submit=簽章 PDF
|
certSign.submit=簽章 PDF
|
||||||
|
|
||||||
|
|
||||||
@@ -786,9 +786,9 @@ compare.highlightColor.2=標示顏色 2:
|
|||||||
compare.document.1=文件 1
|
compare.document.1=文件 1
|
||||||
compare.document.2=文件 2
|
compare.document.2=文件 2
|
||||||
compare.submit=比較
|
compare.submit=比較
|
||||||
compare.complex.message=One or both of the provided documents are large files, accuracy of comparison may be reduced
|
compare.complex.message=選擇的檔案大小太大(其中一個或兩者皆是),可能會影響比較的精確度
|
||||||
compare.large.file.message=One or Both of the provided documents are too large to process
|
compare.large.file.message=選擇的檔案大小超出系統限制(其中一個或兩者皆是),無法處理
|
||||||
compare.no.text.message=One or both of the selected PDFs have no text content. Please choose PDFs with text for comparison.
|
compare.no.text.message=選擇的 PDF 檔案未包含文字(其中一個或兩者皆是)。請選擇含有文字的 PDF 進行比較
|
||||||
|
|
||||||
#BookToPDF
|
#BookToPDF
|
||||||
BookToPDF.title=電子書和漫畫轉 PDF
|
BookToPDF.title=電子書和漫畫轉 PDF
|
||||||
@@ -805,17 +805,17 @@ PDFToBook.submit=轉換
|
|||||||
|
|
||||||
#sign
|
#sign
|
||||||
sign.title=簽章
|
sign.title=簽章
|
||||||
sign.header=簽章 PDF
|
sign.header=簽署 PDF
|
||||||
sign.upload=上傳影像
|
sign.upload=上傳影像
|
||||||
sign.draw=繪製簽章
|
sign.draw=繪製簽章
|
||||||
sign.text=文字輸入
|
sign.text=文字輸入
|
||||||
sign.clear=清除
|
sign.clear=清除
|
||||||
sign.add=新增
|
sign.add=新增
|
||||||
sign.saved=Saved Signatures
|
sign.saved=已儲存的簽章
|
||||||
sign.save=Save Signature
|
sign.save=儲存簽章
|
||||||
sign.personalSigs=Personal Signatures
|
sign.personalSigs=個人簽章
|
||||||
sign.sharedSigs=Shared Signatures
|
sign.sharedSigs=共用簽章
|
||||||
sign.noSavedSigs=No saved signatures found
|
sign.noSavedSigs=尚未儲存任何簽章
|
||||||
|
|
||||||
|
|
||||||
#repair
|
#repair
|
||||||
|
|||||||
@@ -102,3 +102,22 @@ metrics:
|
|||||||
AutomaticallyGenerated:
|
AutomaticallyGenerated:
|
||||||
key: example
|
key: example
|
||||||
UUID: 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
|
||||||
|
|||||||
@@ -126,3 +126,12 @@ html[dir="rtl"] .pdf-actions_container:last-child>.pdf-actions_insert-file-butto
|
|||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
border-radius: 100px;
|
border-radius: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pdf-actions_insert-file-blank-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 75%;
|
||||||
|
right: 50%;
|
||||||
|
translate: 0% -50%;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
border-radius: 100px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ function syncFavorites() {
|
|||||||
cards.forEach(card => {
|
cards.forEach(card => {
|
||||||
const isFavorite = localStorage.getItem(card.id) === "favorite";
|
const isFavorite = localStorage.getItem(card.id) === "favorite";
|
||||||
const starIcon = card.querySelector(".favorite-icon span.material-symbols-rounded");
|
const starIcon = card.querySelector(".favorite-icon span.material-symbols-rounded");
|
||||||
|
if (starIcon) {
|
||||||
if (isFavorite) {
|
if (isFavorite) {
|
||||||
starIcon.classList.remove("no-fill");
|
starIcon.classList.remove("no-fill");
|
||||||
starIcon.classList.add("fill");
|
starIcon.classList.add("fill");
|
||||||
@@ -92,6 +93,7 @@ function syncFavorites() {
|
|||||||
starIcon.classList.add("no-fill");
|
starIcon.classList.add("no-fill");
|
||||||
card.classList.remove("favorite");
|
card.classList.remove("favorite");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
updateFavoritesSection();
|
updateFavoritesSection();
|
||||||
updateFavoritesDropdown();
|
updateFavoritesDropdown();
|
||||||
|
|||||||
@@ -21,27 +21,55 @@ async function displayFiles(files) {
|
|||||||
for (let i = 0; i < files.length; i++) {
|
for (let i = 0; i < files.length; i++) {
|
||||||
const pageCount = await getPDFPageCount(files[i]);
|
const pageCount = await getPDFPageCount(files[i]);
|
||||||
const pageLabel = pageCount === 1 ? pageTranslation : pagesTranslation;
|
const pageLabel = pageCount === 1 ? pageTranslation : pagesTranslation;
|
||||||
|
|
||||||
|
// Create list item
|
||||||
const item = document.createElement("li");
|
const item = document.createElement("li");
|
||||||
item.className = "list-group-item";
|
item.className = "list-group-item";
|
||||||
item.innerHTML = `
|
|
||||||
<div class="d-flex justify-content-between align-items-center w-100">
|
// Create filename div and set textContent to sanitize
|
||||||
<div class="filename">${files[i].name}</div>
|
const fileNameDiv = document.createElement("div");
|
||||||
<div class="page-info">
|
fileNameDiv.className = "filename";
|
||||||
<span class="page-count">${pageCount} ${pageLabel}</span>
|
fileNameDiv.textContent = files[i].name;
|
||||||
</div>
|
|
||||||
<div class="arrows d-flex">
|
// Create page info div and set textContent to sanitize
|
||||||
<button class="btn btn-secondary move-up"><span>↑</span></button>
|
const pageInfoDiv = document.createElement("div");
|
||||||
<button class="btn btn-secondary move-down"><span>↓</span></button>
|
pageInfoDiv.className = "page-info";
|
||||||
<button class="btn btn-danger remove-file"><span>×</span></button>
|
const pageCountSpan = document.createElement("span");
|
||||||
</div>
|
pageCountSpan.className = "page-count";
|
||||||
</div>
|
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>↑</span>";
|
||||||
|
|
||||||
|
const moveDownButton = document.createElement("button");
|
||||||
|
moveDownButton.className = "btn btn-secondary move-down";
|
||||||
|
moveDownButton.innerHTML = "<span>↓</span>";
|
||||||
|
|
||||||
|
const removeButton = document.createElement("button");
|
||||||
|
removeButton.className = "btn btn-danger remove-file";
|
||||||
|
removeButton.innerHTML = "<span>×</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);
|
list.appendChild(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
attachMoveButtons();
|
attachMoveButtons();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function getPDFPageCount(file) {
|
async function getPDFPageCount(file) {
|
||||||
const blobUrl = URL.createObjectURL(file);
|
const blobUrl = URL.createObjectURL(file);
|
||||||
const pdf = await pdfjsLib.getDocument(blobUrl).promise;
|
const pdf = await pdfjsLib.getDocument(blobUrl).promise;
|
||||||
|
|||||||
@@ -73,6 +73,11 @@ class PdfActionsManager {
|
|||||||
this.addFiles(imgContainer);
|
this.addFiles(imgContainer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
insertFileBlankButtonCallback(e) {
|
||||||
|
var imgContainer = this.getPageContainer(e.target);
|
||||||
|
this.addFiles(imgContainer, true);
|
||||||
|
}
|
||||||
|
|
||||||
splitFileButtonCallback(e) {
|
splitFileButtonCallback(e) {
|
||||||
var imgContainer = this.getPageContainer(e.target);
|
var imgContainer = this.getPageContainer(e.target);
|
||||||
imgContainer.classList.toggle("split-before");
|
imgContainer.classList.toggle("split-before");
|
||||||
@@ -89,6 +94,7 @@ class PdfActionsManager {
|
|||||||
this.rotateCWButtonCallback = this.rotateCWButtonCallback.bind(this);
|
this.rotateCWButtonCallback = this.rotateCWButtonCallback.bind(this);
|
||||||
this.deletePageButtonCallback = this.deletePageButtonCallback.bind(this);
|
this.deletePageButtonCallback = this.deletePageButtonCallback.bind(this);
|
||||||
this.insertFileButtonCallback = this.insertFileButtonCallback.bind(this);
|
this.insertFileButtonCallback = this.insertFileButtonCallback.bind(this);
|
||||||
|
this.insertFileBlankButtonCallback = this.insertFileBlankButtonCallback.bind(this);
|
||||||
this.splitFileButtonCallback = this.splitFileButtonCallback.bind(this);
|
this.splitFileButtonCallback = this.splitFileButtonCallback.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,6 +158,12 @@ class PdfActionsManager {
|
|||||||
splitFileButton.onclick = this.splitFileButtonCallback;
|
splitFileButton.onclick = this.splitFileButtonCallback;
|
||||||
insertFileButtonContainer.appendChild(splitFileButton);
|
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);
|
div.appendChild(insertFileButtonContainer);
|
||||||
|
|
||||||
// add this button to every element, but only show it on the last one :D
|
// add this button to every element, but only show it on the last one :D
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ class PdfContainer {
|
|||||||
this.nameAndArchiveFiles = this.nameAndArchiveFiles.bind(this);
|
this.nameAndArchiveFiles = this.nameAndArchiveFiles.bind(this);
|
||||||
this.splitPDF = this.splitPDF.bind(this);
|
this.splitPDF = this.splitPDF.bind(this);
|
||||||
this.splitAll = this.splitAll.bind(this);
|
this.splitAll = this.splitAll.bind(this);
|
||||||
|
this.addFilesBlankAll = this.addFilesBlankAll.bind(this)
|
||||||
|
|
||||||
this.pdfAdapters = pdfAdapters;
|
this.pdfAdapters = pdfAdapters;
|
||||||
|
|
||||||
@@ -38,6 +39,7 @@ class PdfContainer {
|
|||||||
window.exportPdf = this.exportPdf;
|
window.exportPdf = this.exportPdf;
|
||||||
window.rotateAll = this.rotateAll;
|
window.rotateAll = this.rotateAll;
|
||||||
window.splitAll = this.splitAll;
|
window.splitAll = this.splitAll;
|
||||||
|
window.addFilesBlankAll = this.addFilesBlankAll
|
||||||
|
|
||||||
const filenameInput = document.getElementById("filename-input");
|
const filenameInput = document.getElementById("filename-input");
|
||||||
const downloadBtn = document.getElementById("export-button");
|
const downloadBtn = document.getElementById("export-button");
|
||||||
@@ -77,7 +79,12 @@ class PdfContainer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addFiles(nextSiblingElement) {
|
addFiles(nextSiblingElement, blank = false) {
|
||||||
|
if (blank) {
|
||||||
|
|
||||||
|
this.addFilesBlank(nextSiblingElement);
|
||||||
|
|
||||||
|
} else {
|
||||||
var input = document.createElement("input");
|
var input = document.createElement("input");
|
||||||
input.type = "file";
|
input.type = "file";
|
||||||
input.multiple = true;
|
input.multiple = true;
|
||||||
@@ -91,6 +98,7 @@ class PdfContainer {
|
|||||||
|
|
||||||
input.click();
|
input.click();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async addFilesFromFiles(files, nextSiblingElement) {
|
async addFilesFromFiles(files, nextSiblingElement) {
|
||||||
this.fileName = files[0].name;
|
this.fileName = files[0].name;
|
||||||
@@ -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) {
|
rotateElement(element, deg) {
|
||||||
var lastTransform = element.style.rotate;
|
var lastTransform = element.style.rotate;
|
||||||
if (!lastTransform) {
|
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() {
|
splitAll() {
|
||||||
const allPages = this.pagesContainer.querySelectorAll(".page-container");
|
const allPages = this.pagesContainer.querySelectorAll(".page-container");
|
||||||
if (this.pagesContainer.querySelectorAll(".split-before").length > 0) {
|
if (this.pagesContainer.querySelectorAll(".split-before").length > 0) {
|
||||||
|
|||||||
@@ -70,3 +70,37 @@ document.querySelector("#navbarSearchInput").addEventListener("input", function
|
|||||||
resultsBox.style.width = window.navItemMaxWidth + "px";
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -90,8 +90,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-check m-2 mb-3">
|
<div class="form-check m-2 mb-3">
|
||||||
<input type="checkbox" id="remember" value="remember-me">
|
<input type="checkbox" name="remember-me" id="remember-me">
|
||||||
<label for="remember" th:text="#{login.rememberme}"></label>
|
<label for="remember-me" th:text="#{login.rememberme}"></label>
|
||||||
</div>
|
</div>
|
||||||
<button class="w-100 btn btn-lg btn-primary" type="submit" th:text="#{login.signin}">Sign in</button>
|
<button class="w-100 btn btn-lg btn-primary" type="submit" th:text="#{login.signin}">Sign in</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -47,6 +47,11 @@
|
|||||||
cut
|
cut
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</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>
|
<button id="export-button" class="btn btn-primary enable-on-file" onclick="exportPdf()" disabled>
|
||||||
<span class="material-symbols-rounded">
|
<span class="material-symbols-rounded">
|
||||||
download
|
download
|
||||||
|
|||||||
Reference in New Issue
Block a user