Compare commits

..

6 Commits

Author SHA1 Message Date
Dario Ghunney Ware
fe30355242 DOCKER_ENABLE_SECURITY > ADDITIONAL_FEATURES_OFF 2025-05-28 16:48:23 +01:00
Dario Ghunney Ware
5d34f2ff7e added proprietary module to base packages for scanning
clean up
2025-05-28 16:46:14 +01:00
Dario Ghunney Ware
6258f26a1c updating license 2025-05-28 16:46:14 +01:00
Dario Ghunney Ware
8e7758fe26 moving security package and relevant files over to proprietary 2025-05-28 16:45:59 +01:00
Dario Ghunney Ware
8066bf18bd creating new proprietary module 2025-05-28 16:45:09 +01:00
Dario Ghunney Ware
9277715994 wip - making db and sessions conditional 2025-05-28 09:07:07 +01:00
20 changed files with 380 additions and 1563 deletions

View File

@ -36,7 +36,6 @@ jobs:
id: get-pr-data id: get-pr-data
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with: with:
github-token: ${{ steps.setup-bot.outputs.token }}
script: | script: |
const prNumber = context.payload.pull_request.number; const prNumber = context.payload.pull_request.number;
const repoOwner = context.payload.repository.owner.login; const repoOwner = context.payload.repository.owner.login;
@ -57,7 +56,7 @@ jobs:
- name: Fetch PR changed files - name: Fetch PR changed files
id: fetch-pr-changes id: fetch-pr-changes
env: env:
GH_TOKEN: ${{ steps.setup-bot.outputs.token }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
echo "Fetching PR changed files..." echo "Fetching PR changed files..."
echo "Getting list of changed files from PR..." echo "Getting list of changed files from PR..."
@ -67,7 +66,6 @@ jobs:
id: determine-file id: determine-file
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with: with:
github-token: ${{ steps.setup-bot.outputs.token }}
script: | script: |
const fs = require("fs"); const fs = require("fs");
const path = require("path"); const path = require("path");
@ -208,7 +206,6 @@ jobs:
if: env.SCRIPT_OUTPUT != '' if: env.SCRIPT_OUTPUT != ''
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with: with:
github-token: ${{ steps.setup-bot.outputs.token }}
script: | script: |
const { GITHUB_REPOSITORY, SCRIPT_OUTPUT } = process.env; const { GITHUB_REPOSITORY, SCRIPT_OUTPUT } = process.env;
const [repoOwner, repoName] = GITHUB_REPOSITORY.split('/'); const [repoOwner, repoName] = GITHUB_REPOSITORY.split('/');

View File

@ -30,7 +30,7 @@ jobs:
id: setup-bot id: setup-bot
uses: ./.github/actions/setup-bot uses: ./.github/actions/setup-bot
with: with:
app-id: ${{ secrets.GH_APP_ID }} app-id: ${{ vars.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Set up Python - name: Set up Python

View File

@ -50,10 +50,8 @@
".vscode/", ".vscode/",
"bin/", "bin/",
"common/bin/", "common/bin/",
"proprietary/bin/",
"build/", "build/",
"common/build/", "common/build/",
"proprietary/build/",
"configs/", "configs/",
"customFiles/", "customFiles/",
"docs/", "docs/",
@ -68,7 +66,6 @@
".gitattributes", ".gitattributes",
".gitignore", ".gitignore",
"common/.gitignore", "common/.gitignore",
"proprietary/.gitignore",
".pre-commit-config.yaml", ".pre-commit-config.yaml",
], ],
// Enables signature help in Java. // Enables signature help in Java.

View File

@ -117,45 +117,45 @@ Stirling-PDF currently supports 40 languages!
| Language | Progress | | Language | Progress |
| -------------------------------------------- | -------------------------------------- | | -------------------------------------------- | -------------------------------------- |
| Arabic (العربية) (ar_AR) | ![75%](https://geps.dev/progress/75) | | Arabic (العربية) (ar_AR) | ![75%](https://geps.dev/progress/75) |
| Azerbaijani (Azərbaycan Dili) (az_AZ) | ![75%](https://geps.dev/progress/75) | | Azerbaijani (Azərbaycan Dili) (az_AZ) | ![74%](https://geps.dev/progress/74) |
| Basque (Euskara) (eu_ES) | ![44%](https://geps.dev/progress/44) | | Basque (Euskara) (eu_ES) | ![43%](https://geps.dev/progress/43) |
| Bulgarian (Български) (bg_BG) | ![83%](https://geps.dev/progress/83) | | Bulgarian (Български) (bg_BG) | ![83%](https://geps.dev/progress/83) |
| Catalan (Català) (ca_CA) | ![82%](https://geps.dev/progress/82) | | Catalan (Català) (ca_CA) | ![80%](https://geps.dev/progress/80) |
| Croatian (Hrvatski) (hr_HR) | ![74%](https://geps.dev/progress/74) | | Croatian (Hrvatski) (hr_HR) | ![72%](https://geps.dev/progress/72) |
| Czech (Česky) (cs_CZ) | ![85%](https://geps.dev/progress/85) | | Czech (Česky) (cs_CZ) | ![82%](https://geps.dev/progress/82) |
| Danish (Dansk) (da_DK) | ![75%](https://geps.dev/progress/75) | | Danish (Dansk) (da_DK) | ![71%](https://geps.dev/progress/71) |
| Dutch (Nederlands) (nl_NL) | ![73%](https://geps.dev/progress/73) | | Dutch (Nederlands) (nl_NL) | ![71%](https://geps.dev/progress/71) |
| English (English) (en_GB) | ![100%](https://geps.dev/progress/100) | | English (English) (en_GB) | ![100%](https://geps.dev/progress/100) |
| English (US) (en_US) | ![100%](https://geps.dev/progress/100) | | English (US) (en_US) | ![100%](https://geps.dev/progress/100) |
| French (Français) (fr_FR) | ![84%](https://geps.dev/progress/84) | | French (Français) (fr_FR) | ![82%](https://geps.dev/progress/82) |
| German (Deutsch) (de_DE) | ![91%](https://geps.dev/progress/91) | | German (Deutsch) (de_DE) | ![89%](https://geps.dev/progress/89) |
| Greek (Ελληνικά) (el_GR) | ![82%](https://geps.dev/progress/82) | | Greek (Ελληνικά) (el_GR) | ![81%](https://geps.dev/progress/81) |
| Hindi (हिंदी) (hi_IN) | ![82%](https://geps.dev/progress/82) | | Hindi (हिंदी) (hi_IN) | ![82%](https://geps.dev/progress/82) |
| Hungarian (Magyar) (hu_HU) | ![89%](https://geps.dev/progress/89) | | Hungarian (Magyar) (hu_HU) | ![88%](https://geps.dev/progress/88) |
| Indonesian (Bahasa Indonesia) (id_ID) | ![75%](https://geps.dev/progress/75) | | Indonesian (Bahasa Indonesia) (id_ID) | ![72%](https://geps.dev/progress/72) |
| Irish (Gaeilge) (ga_IE) | ![83%](https://geps.dev/progress/83) | | Irish (Gaeilge) (ga_IE) | ![82%](https://geps.dev/progress/82) |
| Italian (Italiano) (it_IT) | ![98%](https://geps.dev/progress/98) | | Italian (Italiano) (it_IT) | ![96%](https://geps.dev/progress/96) |
| Japanese (日本語) (ja_JP) | ![84%](https://geps.dev/progress/84) | | Japanese (日本語) (ja_JP) | ![84%](https://geps.dev/progress/84) |
| Korean (한국어) (ko_KR) | ![82%](https://geps.dev/progress/82) | | Korean (한국어) (ko_KR) | ![82%](https://geps.dev/progress/82) |
| Norwegian (Norsk) (no_NB) | ![80%](https://geps.dev/progress/80) | | Norwegian (Norsk) (no_NB) | ![77%](https://geps.dev/progress/77) |
| Persian (فارسی) (fa_IR) | ![78%](https://geps.dev/progress/78) | | Persian (فارسی) (fa_IR) | ![78%](https://geps.dev/progress/78) |
| Polish (Polski) (pl_PL) | ![88%](https://geps.dev/progress/88) | | Polish (Polski) (pl_PL) | ![85%](https://geps.dev/progress/85) |
| Portuguese (Português) (pt_PT) | ![84%](https://geps.dev/progress/84) | | Portuguese (Português) (pt_PT) | ![82%](https://geps.dev/progress/82) |
| Portuguese Brazilian (Português) (pt_BR) | ![89%](https://geps.dev/progress/89) | | Portuguese Brazilian (Português) (pt_BR) | ![87%](https://geps.dev/progress/87) |
| Romanian (Română) (ro_RO) | ![70%](https://geps.dev/progress/70) | | Romanian (Română) (ro_RO) | ![67%](https://geps.dev/progress/67) |
| Russian (Русский) (ru_RU) | ![89%](https://geps.dev/progress/89) | | Russian (Русский) (ru_RU) | ![89%](https://geps.dev/progress/89) |
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) | ![53%](https://geps.dev/progress/53) | | Serbian Latin alphabet (Srpski) (sr_LATN_RS) | ![53%](https://geps.dev/progress/53) |
| Simplified Chinese (简体中文) (zh_CN) | ![88%](https://geps.dev/progress/88) | | Simplified Chinese (简体中文) (zh_CN) | ![88%](https://geps.dev/progress/88) |
| Slovakian (Slovensky) (sk_SK) | ![63%](https://geps.dev/progress/63) | | Slovakian (Slovensky) (sk_SK) | ![61%](https://geps.dev/progress/61) |
| Slovenian (Slovenščina) (sl_SI) | ![87%](https://geps.dev/progress/87) | | Slovenian (Slovenščina) (sl_SI) | ![84%](https://geps.dev/progress/84) |
| Spanish (Español) (es_ES) | ![91%](https://geps.dev/progress/91) | | Spanish (Español) (es_ES) | ![89%](https://geps.dev/progress/89) |
| Swedish (Svenska) (sv_SE) | ![80%](https://geps.dev/progress/80) | | Swedish (Svenska) (sv_SE) | ![78%](https://geps.dev/progress/78) |
| Thai (ไทย) (th_TH) | ![72%](https://geps.dev/progress/72) | | Thai (ไทย) (th_TH) | ![71%](https://geps.dev/progress/71) |
| Tibetan (བོད་ཡིག་) (bo_CN) | ![79%](https://geps.dev/progress/79) | | Tibetan (བོད་ཡིག་) (zh_BO) | ![79%](https://geps.dev/progress/79) |
| Traditional Chinese (繁體中文) (zh_TW) | ![89%](https://geps.dev/progress/89) | | Traditional Chinese (繁體中文) (zh_TW) | ![89%](https://geps.dev/progress/89) |
| Turkish (Türkçe) (tr_TR) | ![90%](https://geps.dev/progress/90) | | Turkish (Türkçe) (tr_TR) | ![87%](https://geps.dev/progress/87) |
| Ukrainian (Українська) (uk_UA) | ![89%](https://geps.dev/progress/89) | | Ukrainian (Українська) (uk_UA) | ![89%](https://geps.dev/progress/89) |
| Vietnamese (Tiếng Việt) (vi_VN) | ![70%](https://geps.dev/progress/70) | | Vietnamese (Tiếng Việt) (vi_VN) | ![66%](https://geps.dev/progress/66) |
| Malayalam (മലയാളം) (ml_IN) | ![89%](https://geps.dev/progress/89) | | Malayalam (മലയാളം) (ml_IN) | ![89%](https://geps.dev/progress/89) |
## Stirling PDF Enterprise ## Stirling PDF Enterprise

View File

@ -354,7 +354,6 @@ spotless {
java { java {
target sourceSets.main.allJava target sourceSets.main.allJava
target project(':common').sourceSets.main.allJava target project(':common').sourceSets.main.allJava
target project(':proprietary').sourceSets.main.allJava
googleJavaFormat("1.27.0").aosp().reorderImports(false) googleJavaFormat("1.27.0").aosp().reorderImports(false)

View File

@ -10,7 +10,6 @@ import java.util.Properties;
import java.util.function.Predicate; import java.util.function.Predicate;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@ -147,16 +146,8 @@ public class AppConfig {
} }
} }
@ConditionalOnMissingClass("stirling.software.SPDF.config.security.SecurityConfiguration")
@Bean(name = "activeSecurity") @Bean(name = "activeSecurity")
@ConditionalOnClass(
name = "stirling.software.proprietary.security.configuration.SecurityConfiguration")
public boolean activeSecurity() {
return true;
}
@Bean(name = "missingActiveSecurity")
@ConditionalOnMissingClass(
"stirling.software.proprietary.security.configuration.SecurityConfiguration")
public boolean missingActiveSecurity() { public boolean missingActiveSecurity() {
return false; return false;
} }

View File

@ -14,8 +14,11 @@ covering the appropriate number of licensed users.
Trial and Minimal Use Trial and Minimal Use
You may use the Software without a paid subscription for the sole purposes of internal trial, evaluation, or minimal use, provided that: You may use the Software without a paid subscription for the sole purposes of internal trial, evaluation, or minimal use,
* Use is limited to the capabilities and restrictions defined by the Software itself; provided that:
* Use is limited to no more than five (5) named users or sessions;
* Use is strictly non-production, for internal testing or evaluation only;
* You do not copy, distribute, sublicense, reverse-engineer, or use the Software in client-facing or commercial contexts. * You do not copy, distribute, sublicense, reverse-engineer, or use the Software in client-facing or commercial contexts.
Continued use beyond this scope requires a valid Stirling PDF User License. Continued use beyond this scope requires a valid Stirling PDF User License.

View File

@ -26,17 +26,17 @@ dependencyManagement {
dependencies { dependencies {
implementation project(':common') implementation project(':common')
api 'org.springframework:spring-jdbc' implementation 'org.springframework:spring-jdbc'
api 'org.springframework:spring-webmvc' implementation 'org.springframework:spring-webmvc'
api 'org.springframework.session:spring-session-core' implementation 'org.springframework.session:spring-session-core'
api "org.springframework.security:spring-security-core:$springSecuritySamlVersion" implementation "org.springframework.security:spring-security-core:$springSecuritySamlVersion"
api "org.springframework.security:spring-security-saml2-service-provider:$springSecuritySamlVersion" implementation "org.springframework.security:spring-security-saml2-service-provider:$springSecuritySamlVersion"
api 'org.springframework.boot:spring-boot-starter-jetty' implementation 'org.springframework.boot:spring-boot-starter-jetty'
api 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-security'
api 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
api 'org.springframework.boot:spring-boot-starter-oauth2-client' implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
api 'org.springframework.boot:spring-boot-starter-mail' implementation 'org.springframework.boot:spring-boot-starter-mail'
api 'io.swagger.core.v3:swagger-core-jakarta:2.2.30' implementation 'io.swagger.core.v3:swagger-core-jakarta:2.2.30'
implementation 'com.bucket4j:bucket4j_jdk17-core:8.14.0' implementation 'com.bucket4j:bucket4j_jdk17-core:8.14.0'
// https://mvnrepository.com/artifact/com.bucket4j/bucket4j_jdk17 // https://mvnrepository.com/artifact/com.bucket4j/bucket4j_jdk17
@ -44,7 +44,7 @@ dependencies {
implementation 'io.github.pixee:java-security-toolkit:1.2.1' implementation 'io.github.pixee:java-security-toolkit:1.2.1'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.3.RELEASE' implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.3.RELEASE'
api 'io.micrometer:micrometer-registry-prometheus' implementation 'io.micrometer:micrometer-registry-prometheus'
implementation 'com.unboundid.product.scim2:scim2-sdk-client:2.3.5' implementation 'com.unboundid.product.scim2:scim2-sdk-client:2.3.5'
runtimeOnly 'com.h2database:h2:2.3.232' // Don't upgrade h2database runtimeOnly 'com.h2database:h2:2.3.232' // Don't upgrade h2database
runtimeOnly 'org.postgresql:postgresql:42.7.5' runtimeOnly 'org.postgresql:postgresql:42.7.5'

View File

@ -182,8 +182,7 @@ def compare_files(
sort_ignore_translation[language]["ignore"].remove( sort_ignore_translation[language]["ignore"].remove(
default_key.strip() default_key.strip()
) )
except ValueError as e: except ValueError:
print(f"Error processing line {line_num} in {file_path}: {e}")
print(f"{line_default}|{line_file}") print(f"{line_default}|{line_file}")
exit(1) exit(1)
except IndexError: except IndexError:

View File

@ -1,190 +1,34 @@
[ar_AR] [ar_AR]
ignore = [ ignore = [
'lang.div',
'lang.dzo',
'lang.que',
'language.direction', 'language.direction',
] ]
[az_AZ] [az_AZ]
ignore = [ ignore = [
'lang.afr',
'lang.bre',
'lang.div',
'lang.epo',
'lang.guj',
'lang.hin',
'lang.kan',
'lang.mal',
'lang.mar',
'lang.mlt',
'lang.mri',
'lang.msa',
'lang.nep',
'lang.ori',
'lang.pan',
'lang.san',
'lang.sin',
'lang.slk',
'lang.snd',
'lang.sun',
'lang.tam',
'lang.tat',
'lang.urd',
'lang.yor',
'language.direction', 'language.direction',
] ]
[bg_BG] [bg_BG]
ignore = [
'lang.div',
'lang.dzo',
'lang.iku',
'lang.que',
'language.direction',
]
[bo_CN]
ignore = [ ignore = [
'language.direction', 'language.direction',
] ]
[ca_CA] [ca_CA]
ignore = [ ignore = [
'PDFToText.tags',
'adminUserSettings.admin', 'adminUserSettings.admin',
'lang.amh',
'lang.ceb',
'lang.chr',
'lang.div',
'lang.dzo',
'lang.epo',
'lang.fao',
'lang.fry',
'lang.guj',
'lang.hin',
'lang.iku',
'lang.kan',
'lang.kaz',
'lang.lao',
'lang.mar',
'lang.mri',
'lang.ori',
'lang.pan',
'lang.pus',
'lang.que',
'lang.snd',
'lang.swa',
'lang.tam',
'lang.tat',
'lang.tel',
'lang.tgk',
'lang.tgl',
'lang.tir',
'lang.uzb',
'lang.uzb_cyrl',
'language.direction', 'language.direction',
'watermark.type.1', 'watermark.type.1',
] ]
[cs_CZ] [cs_CZ]
ignore = [ ignore = [
'lang.amh',
'lang.asm',
'lang.bod',
'lang.bos',
'lang.bre',
'lang.ceb',
'lang.chr',
'lang.div',
'lang.dzo',
'lang.frk',
'lang.gla',
'lang.guj',
'lang.iku',
'lang.jav',
'lang.kan',
'lang.kat',
'lang.khm',
'lang.kir',
'lang.lao',
'lang.mal',
'lang.mar',
'lang.mri',
'lang.msa',
'lang.nor',
'lang.oci',
'lang.ori',
'lang.pan',
'lang.pus',
'lang.que',
'lang.san',
'lang.sin',
'lang.snd',
'lang.sun',
'lang.tam',
'lang.tat',
'lang.tel',
'lang.tgl',
'lang.tha',
'lang.tir',
'lang.uig',
'lang.urd',
'lang.uzb',
'lang.uzb_cyrl',
'lang.yor',
'language.direction', 'language.direction',
'text', 'text',
] ]
[da_DK] [da_DK]
ignore = [ ignore = [
'lang.afr',
'lang.amh',
'lang.ben',
'lang.bre',
'lang.ceb',
'lang.chr',
'lang.div',
'lang.dzo',
'lang.epo',
'lang.frk',
'lang.guj',
'lang.hin',
'lang.iku',
'lang.jav',
'lang.kan',
'lang.khm',
'lang.lao',
'lang.lat',
'lang.ltz',
'lang.mal',
'lang.mar',
'lang.mri',
'lang.msa',
'lang.nep',
'lang.oci',
'lang.ori',
'lang.pan',
'lang.pus',
'lang.que',
'lang.san',
'lang.sin',
'lang.slk_frak',
'lang.snd',
'lang.sun',
'lang.swa',
'lang.tam',
'lang.tat',
'lang.tel',
'lang.tgk',
'lang.tgl',
'lang.tha',
'lang.tir',
'lang.ton',
'lang.uig',
'lang.urd',
'lang.uzb',
'lang.yor',
'language.direction', 'language.direction',
] ]
@ -197,36 +41,8 @@ ignore = [
'addPageNumbers.selectText.3', 'addPageNumbers.selectText.3',
'alphabet', 'alphabet',
'certSign.name', 'certSign.name',
'cookieBanner.popUp.acceptAllBtn',
'endpointStatistics.top10',
'endpointStatistics.top20',
'fileChooser.dragAndDrop', 'fileChooser.dragAndDrop',
'home.pipeline.title', 'home.pipeline.title',
'lang.afr',
'lang.ceb',
'lang.chr',
'lang.div',
'lang.dzo',
'lang.epo',
'lang.guj',
'lang.hin',
'lang.iku',
'lang.kan',
'lang.mal',
'lang.mar',
'lang.mri',
'lang.nep',
'lang.ori',
'lang.pan',
'lang.que',
'lang.san',
'lang.snd',
'lang.tam',
'lang.tel',
'lang.tgl',
'lang.tir',
'lang.urd',
'lang.yor',
'language.direction', 'language.direction',
'legal.impressum', 'legal.impressum',
'licenses.version', 'licenses.version',
@ -240,19 +56,13 @@ ignore = [
'validateSignature.cert.version', 'validateSignature.cert.version',
'validateSignature.status', 'validateSignature.status',
'watermark.type.1', 'watermark.type.1',
'endpointStatistics.top10',
'endpointStatistics.top20',
'cookieBanner.popUp.acceptAllBtn',
] ]
[el_GR] [el_GR]
ignore = [ ignore = [
'lang.ceb',
'lang.dzo',
'lang.iku',
'lang.ori',
'lang.pan',
'lang.que',
'lang.sin',
'lang.uig',
'lang.uzb_cyrl',
'language.direction', 'language.direction',
] ]
@ -260,31 +70,6 @@ ignore = [
ignore = [ ignore = [
'adminUserSettings.roles', 'adminUserSettings.roles',
'error', 'error',
'lang.asm',
'lang.ceb',
'lang.chr',
'lang.div',
'lang.dzo',
'lang.fil',
'lang.frm',
'lang.guj',
'lang.iku',
'lang.kan',
'lang.lao',
'lang.mal',
'lang.mar',
'lang.oci',
'lang.ori',
'lang.pan',
'lang.san',
'lang.snd',
'lang.sun',
'lang.tam',
'lang.tel',
'lang.tir',
'lang.urd',
'lang.uzb',
'lang.yor',
'language.direction', 'language.direction',
'no', 'no',
'showJS.tags', 'showJS.tags',
@ -292,23 +77,6 @@ ignore = [
[eu_ES] [eu_ES]
ignore = [ ignore = [
'lang.ceb',
'lang.chr',
'lang.div',
'lang.dzo',
'lang.guj',
'lang.iku',
'lang.kan',
'lang.mal',
'lang.pan',
'lang.que',
'lang.san',
'lang.slv',
'lang.snd',
'lang.sqi',
'lang.tat',
'lang.tir',
'lang.yor',
'language.direction', 'language.direction',
] ]
@ -328,31 +96,6 @@ ignore = [
'alphabet', 'alphabet',
'compare.document.1', 'compare.document.1',
'compare.document.2', 'compare.document.2',
'lang.bre',
'lang.ceb',
'lang.chr',
'lang.div',
'lang.dzo',
'lang.eus',
'lang.guj',
'lang.iku',
'lang.kan',
'lang.kaz',
'lang.khm',
'lang.lao',
'lang.ltz',
'lang.mal',
'lang.mar',
'lang.oci',
'lang.ori',
'lang.que',
'lang.san',
'lang.snd',
'lang.swa',
'lang.tel',
'lang.tgl',
'lang.tir',
'lang.yor',
'language.direction', 'language.direction',
'licenses.license', 'licenses.license',
'licenses.module', 'licenses.module',
@ -365,24 +108,6 @@ ignore = [
[ga_IE] [ga_IE]
ignore = [ ignore = [
'lang.ceb',
'lang.cos',
'lang.div',
'lang.dzo',
'lang.epo',
'lang.guj',
'lang.hat',
'lang.iku',
'lang.lao',
'lang.oci',
'lang.ori',
'lang.pan',
'lang.sin',
'lang.snd',
'lang.sun',
'lang.tgk',
'lang.tir',
'lang.uig',
'language.direction', 'language.direction',
] ]
@ -395,126 +120,22 @@ ignore = [
ignore = [ ignore = [
'PDFToBook.selectText.1', 'PDFToBook.selectText.1',
'home.pipeline.title', 'home.pipeline.title',
'lang.bod',
'lang.bre',
'lang.ceb',
'lang.chr',
'lang.dzo',
'lang.guj',
'lang.iku',
'lang.kan',
'lang.lao',
'lang.mri',
'lang.ori',
'lang.pan',
'lang.pus',
'lang.que',
'lang.san',
'lang.snd',
'lang.tam',
'lang.tel',
'lang.tgl',
'lang.tir',
'language.direction', 'language.direction',
'showJS.tags', 'showJS.tags',
] ]
[hu_HU] [hu_HU]
ignore = [ ignore = [
'lang.bre',
'lang.ceb',
'lang.chr',
'lang.div',
'lang.dzo',
'lang.fao',
'lang.iku',
'lang.kan',
'lang.lao',
'lang.mar',
'lang.mri',
'lang.ori',
'lang.que',
'lang.tel',
'lang.tgl',
'language.direction', 'language.direction',
] ]
[id_ID] [id_ID]
ignore = [ ignore = [
'lang.aze',
'lang.aze_cyrl',
'lang.bre',
'lang.cat',
'lang.ceb',
'lang.chr',
'lang.cym',
'lang.div',
'lang.dzo',
'lang.epo',
'lang.eus',
'lang.fao',
'lang.frk',
'lang.guj',
'lang.hin',
'lang.iku',
'lang.kan',
'lang.kaz',
'lang.kir',
'lang.lao',
'lang.lat',
'lang.mal',
'lang.mar',
'lang.mri',
'lang.ori',
'lang.pan',
'lang.pus',
'lang.que',
'lang.slk_frak',
'lang.snd',
'lang.sun',
'lang.swa',
'lang.tam',
'lang.tat',
'lang.tel',
'lang.tgk',
'lang.tgl',
'lang.tha',
'lang.tir',
'lang.uig',
'lang.urd',
'lang.uzb',
'lang.uzb_cyrl',
'lang.yor',
'language.direction', 'language.direction',
] ]
[it_IT] [it_IT]
ignore = [ ignore = [
'lang.asm',
'lang.aze',
'lang.ceb',
'lang.chr',
'lang.div',
'lang.dzo',
'lang.fao',
'lang.guj',
'lang.iku',
'lang.kan',
'lang.lao',
'lang.mal',
'lang.mar',
'lang.mri',
'lang.ori',
'lang.pan',
'lang.pus',
'lang.que',
'lang.snd',
'lang.swa',
'lang.tam',
'lang.tel',
'lang.tgl',
'lang.urd',
'lang.yor',
'language.direction', 'language.direction',
'no', 'no',
'password', 'password',
@ -527,21 +148,11 @@ ignore = [
[ja_JP] [ja_JP]
ignore = [ ignore = [
'lang.jav',
'language.direction', 'language.direction',
] ]
[ko_KR] [ko_KR]
ignore = [ ignore = [
'lang.fao',
'lang.pus',
'lang.sun',
'language.direction',
]
[ml_IN]
ignore = [
'lang.iku',
'language.direction', 'language.direction',
] ]
@ -549,37 +160,6 @@ ignore = [
ignore = [ ignore = [
'compare.document.1', 'compare.document.1',
'compare.document.2', 'compare.document.2',
'lang.afr',
'lang.asm',
'lang.bre',
'lang.ceb',
'lang.chr',
'lang.dzo',
'lang.epo',
'lang.fao',
'lang.guj',
'lang.hin',
'lang.iku',
'lang.kan',
'lang.lao',
'lang.mal',
'lang.mar',
'lang.mri',
'lang.oci',
'lang.ori',
'lang.pan',
'lang.pus',
'lang.que',
'lang.sin',
'lang.snd',
'lang.sun',
'lang.swa',
'lang.tam',
'lang.tel',
'lang.tgl',
'lang.ton',
'lang.urd',
'lang.yor',
'language.direction', 'language.direction',
'navbar.allTools', 'navbar.allTools',
'sponsor', 'sponsor',
@ -590,49 +170,6 @@ ignore = [
'PDFToBook.selectText.1', 'PDFToBook.selectText.1',
'adminUserSettings.admin', 'adminUserSettings.admin',
'info', 'info',
'lang.afr',
'lang.amh',
'lang.ben',
'lang.bos',
'lang.bre',
'lang.ceb',
'lang.chr',
'lang.dan_frak',
'lang.div',
'lang.dzo',
'lang.epo',
'lang.guj',
'lang.hin',
'lang.iku',
'lang.kan',
'lang.khm',
'lang.lao',
'lang.lat',
'lang.ltz',
'lang.mal',
'lang.mar',
'lang.mri',
'lang.msa',
'lang.nep',
'lang.ori',
'lang.pan',
'lang.pus',
'lang.que',
'lang.san',
'lang.slk_frak',
'lang.snd',
'lang.swa',
'lang.tam',
'lang.tat',
'lang.tel',
'lang.tgk',
'lang.tgl',
'lang.tha',
'lang.tir',
'lang.ton',
'lang.uig',
'lang.urd',
'lang.yor',
'language.direction', 'language.direction',
'oops', 'oops',
'sponsor', 'sponsor',
@ -641,148 +178,27 @@ ignore = [
[pl_PL] [pl_PL]
ignore = [ ignore = [
'PDFToBook.selectText.1', 'PDFToBook.selectText.1',
'lang.afr',
'lang.bre',
'lang.ceb',
'lang.chr',
'lang.cos',
'lang.div',
'lang.dzo',
'lang.fao',
'lang.frk',
'lang.guj',
'lang.hat',
'lang.iku',
'lang.kan',
'lang.khm',
'lang.lao',
'lang.mal',
'lang.mar',
'lang.mri',
'lang.nep',
'lang.oci',
'lang.ori',
'lang.pus',
'lang.que',
'lang.snd',
'lang.sun',
'lang.swa',
'lang.tam',
'lang.tat',
'lang.tel',
'lang.tgl',
'lang.tir',
'lang.uig',
'lang.urd',
'language.direction', 'language.direction',
] ]
[pt_BR] [pt_BR]
ignore = [ ignore = [
'lang.bre',
'lang.ceb',
'lang.chr',
'lang.div',
'lang.dzo',
'lang.fao',
'lang.fil',
'lang.frk',
'lang.fry',
'lang.guj',
'lang.iku',
'lang.kan',
'lang.kir',
'lang.mar',
'lang.ori',
'lang.pan',
'lang.que',
'lang.snd',
'lang.tat',
'lang.tel',
'lang.tgl',
'lang.tir',
'lang.uig',
'lang.uzb',
'lang.yid',
'language.direction', 'language.direction',
'pipelineOptions.pipelineHeader', 'pipelineOptions.pipelineHeader',
] ]
[pt_PT] [pt_PT]
ignore = [ ignore = [
'lang.bre',
'lang.ceb',
'lang.chr',
'lang.div',
'lang.dzo',
'lang.fao',
'lang.fil',
'lang.frk',
'lang.fry',
'lang.guj',
'lang.iku',
'lang.kan',
'lang.kir',
'lang.mar',
'lang.ori',
'lang.pan',
'lang.que',
'lang.snd',
'lang.tat',
'lang.tel',
'lang.tgl',
'lang.tir',
'lang.uig',
'lang.uzb',
'lang.yid',
'language.direction', 'language.direction',
] ]
[ro_RO] [ro_RO]
ignore = [ ignore = [
'lang.amh',
'lang.asm',
'lang.bod',
'lang.bre',
'lang.ceb',
'lang.chr',
'lang.cos',
'lang.deu_frak',
'lang.div',
'lang.dzo',
'lang.est',
'lang.fao',
'lang.glg',
'lang.guj',
'lang.iku',
'lang.jav',
'lang.kan',
'lang.lao',
'lang.mal',
'lang.mar',
'lang.mri',
'lang.nep',
'lang.oci',
'lang.ori',
'lang.pan',
'lang.pus',
'lang.slk_frak',
'lang.snd',
'lang.sun',
'lang.swa',
'lang.tam',
'lang.tel',
'lang.tgl',
'lang.tir',
'lang.urd',
'lang.yor',
'language.direction', 'language.direction',
] ]
[ru_RU] [ru_RU]
ignore = [ ignore = [
'lang.iku',
'lang.pus',
'language.direction', 'language.direction',
] ]
@ -791,25 +207,6 @@ ignore = [
'adminUserSettings.admin', 'adminUserSettings.admin',
'home.multiTool.title', 'home.multiTool.title',
'info', 'info',
'lang.ceb',
'lang.chr',
'lang.dzo',
'lang.epo',
'lang.iku',
'lang.kaz',
'lang.mar',
'lang.ori',
'lang.pan',
'lang.que',
'lang.san',
'lang.sin',
'lang.snd',
'lang.tat',
'lang.tel',
'lang.tgl',
'lang.tir',
'lang.urd',
'lang.uzb',
'language.direction', 'language.direction',
'navbar.sections.security', 'navbar.sections.security',
'text', 'text',
@ -818,37 +215,6 @@ ignore = [
[sl_SI] [sl_SI]
ignore = [ ignore = [
'lang.bre',
'lang.ceb',
'lang.chr',
'lang.div',
'lang.dzo',
'lang.epo',
'lang.eus',
'lang.fao',
'lang.frk',
'lang.guj',
'lang.iku',
'lang.kan',
'lang.lao',
'lang.mar',
'lang.mri',
'lang.oci',
'lang.ori',
'lang.pan',
'lang.que',
'lang.slk',
'lang.snd',
'lang.sun',
'lang.tam',
'lang.tat',
'lang.tel',
'lang.tgk',
'lang.tgl',
'lang.tir',
'lang.urd',
'lang.uzb',
'lang.yor',
'language.direction', 'language.direction',
] ]
@ -861,43 +227,11 @@ ignore = [
[sv_SE] [sv_SE]
ignore = [ ignore = [
'lang.ben',
'lang.bre',
'lang.ceb',
'lang.chr',
'lang.dzo',
'lang.epo',
'lang.guj',
'lang.hin',
'lang.kan',
'lang.lao',
'lang.lat',
'lang.mal',
'lang.mri',
'lang.ori',
'lang.pan',
'lang.que',
'lang.san',
'lang.slk_frak',
'lang.snd',
'lang.tam',
'lang.tat',
'lang.tel',
'lang.tir',
'lang.urd',
'lang.yor',
'language.direction', 'language.direction',
] ]
[th_TH] [th_TH]
ignore = [ ignore = [
'lang.dzo',
'lang.kir',
'lang.pan',
'lang.sin',
'lang.slk_frak',
'lang.tir',
'lang.uzb_cyrl',
'language.direction', 'language.direction',
'pipelineOptions.pipelineHeader', 'pipelineOptions.pipelineHeader',
'showJS.tags', 'showJS.tags',
@ -905,111 +239,33 @@ ignore = [
[tr_TR] [tr_TR]
ignore = [ ignore = [
'lang.afr',
'lang.bre',
'lang.ceb',
'lang.chr',
'lang.div',
'lang.dzo',
'lang.epo',
'lang.fao',
'lang.guj',
'lang.kan',
'lang.lao',
'lang.mal',
'lang.mar',
'lang.mri',
'lang.msa',
'lang.ori',
'lang.pus',
'lang.que',
'lang.sin',
'lang.slk',
'lang.slk_frak',
'lang.snd',
'lang.tam',
'lang.tat',
'lang.tel',
'lang.tgk',
'lang.tgl',
'lang.tir',
'lang.urd',
'lang.yor',
'language.direction', 'language.direction',
] ]
[uk_UA] [uk_UA]
ignore = [ ignore = [
'lang.iku',
'language.direction', 'language.direction',
] ]
[vi_VN] [vi_VN]
ignore = [ ignore = [
'lang.amh',
'lang.asm',
'lang.aze',
'lang.aze_cyrl',
'lang.bos',
'lang.bre',
'lang.cat',
'lang.ceb',
'lang.chr',
'lang.cos',
'lang.div',
'lang.dzo',
'lang.epo',
'lang.eus',
'lang.fao',
'lang.glg',
'lang.guj',
'lang.iku',
'lang.kan',
'lang.kaz',
'lang.kir',
'lang.lat',
'lang.ltz',
'lang.mal',
'lang.mar',
'lang.mri',
'lang.msa',
'lang.ori',
'lang.pus',
'lang.que',
'lang.sin',
'lang.slk',
'lang.slk_frak',
'lang.snd',
'lang.swa',
'lang.syr',
'lang.tam',
'lang.tat',
'lang.tel',
'lang.tgk',
'lang.tir',
'lang.uig',
'lang.uzb',
'lang.uzb_cyrl',
'lang.yid',
'lang.yor',
'language.direction', 'language.direction',
'pipeline.title', 'pipeline.title',
'pipelineOptions.pipelineHeader', 'pipelineOptions.pipelineHeader',
'showJS.tags', 'showJS.tags',
] ]
[zh_BO]
ignore = [
'language.direction',
]
[zh_CN] [zh_CN]
ignore = [ ignore = [
'lang.dzo',
'lang.iku',
'lang.que',
'language.direction', 'language.direction',
] ]
[zh_TW] [zh_TW]
ignore = [ ignore = [
'lang.dzo',
'lang.iku',
'lang.que',
'language.direction', 'language.direction',
] ]

View File

@ -520,7 +520,7 @@ public class KeygenLicenseVerifier {
HttpResponse<String> response = HttpResponse<String> response =
httpClient.send(request, HttpResponse.BodyHandlers.ofString()); httpClient.send(request, HttpResponse.BodyHandlers.ofString());
log.debug("ValidateLicenseResponse body: {}", response.body()); log.info("ValidateLicenseResponse body: {}", response.body());
JsonNode jsonResponse = objectMapper.readTree(response.body()); JsonNode jsonResponse = objectMapper.readTree(response.body());
if (response.statusCode() == 200) { if (response.statusCode() == 200) {
JsonNode metaNode = jsonResponse.path("meta"); JsonNode metaNode = jsonResponse.path("meta");
@ -529,9 +529,9 @@ public class KeygenLicenseVerifier {
String detail = metaNode.path("detail").asText(); String detail = metaNode.path("detail").asText();
String code = metaNode.path("code").asText(); String code = metaNode.path("code").asText();
log.info("License validity: {}", isValid); log.info("License validity: " + isValid);
log.info("Validation detail: {}", detail); log.info("Validation detail: " + detail);
log.info("Validation code: {}", code); log.info("Validation code: " + code);
// Check if the license itself has floating attribute // Check if the license itself has floating attribute
JsonNode licenseAttrs = jsonResponse.path("data").path("attributes"); JsonNode licenseAttrs = jsonResponse.path("data").path("attributes");
@ -595,7 +595,7 @@ public class KeygenLicenseVerifier {
.path("isEnterprise") .path("isEnterprise")
.asBoolean(false); .asBoolean(false);
log.debug(applicationProperties.toString()); log.info(applicationProperties.toString());
} else { } else {
log.error("Error validating license. Status code: {}", response.statusCode()); log.error("Error validating license. Status code: {}", response.statusCode());

View File

@ -1,440 +0,0 @@
package stirling.software.SPDF.controller.api.misc;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Random;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.apache.pdfbox.rendering.PDFRenderer;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.github.pixee.security.Filenames;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.misc.FakeScanRequest;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/misc")
@Tag(name = "Misc", description = "Miscellaneous PDF APIs")
@RequiredArgsConstructor
@Slf4j
public class FakeScanController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private static final Random RANDOM = new Random();
@PostMapping(value = "/fake-scan", consumes = "multipart/form-data")
@Operation(
summary = "Convert PDF to look like a scanned document",
description =
"Applies various effects to make a PDF look like it was scanned, including rotation, noise, and edge softening. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> fakeScan(@Valid @ModelAttribute FakeScanRequest request)
throws IOException {
MultipartFile file = request.getFileInput();
// Apply preset first if needed
if (!request.isAdvancedEnabled()) {
switch (request.getQuality()) {
case high -> request.applyHighQualityPreset();
case medium -> request.applyMediumQualityPreset();
case low -> request.applyLowQualityPreset();
}
}
// Extract values after preset application
int baseRotation = request.getRotationValue() + request.getRotate();
int rotateVariance = request.getRotateVariance();
int borderPx = request.getBorder();
float brightness = request.getBrightness();
float contrast = request.getContrast();
float blur = request.getBlur();
float noise = request.getNoise();
boolean yellowish = request.isYellowish();
int resolution = request.getResolution();
FakeScanRequest.Colorspace colorspace = request.getColorspace();
try (PDDocument document = pdfDocumentFactory.load(file)) {
PDDocument outputDocument = new PDDocument();
PDFRenderer pdfRenderer = new PDFRenderer(document);
for (int i = 0; i < document.getNumberOfPages(); i++) {
// Render page to image with specified resolution
BufferedImage image = pdfRenderer.renderImageWithDPI(i, resolution);
// 1. Convert to grayscale or keep color
BufferedImage processed;
if (colorspace == FakeScanRequest.Colorspace.grayscale) {
processed =
new BufferedImage(
image.getWidth(),
image.getHeight(),
BufferedImage.TYPE_INT_RGB);
Graphics2D gGray = processed.createGraphics();
gGray.setColor(Color.BLACK);
gGray.fillRect(0, 0, image.getWidth(), image.getHeight());
gGray.drawImage(image, 0, 0, null);
gGray.dispose();
// Convert to grayscale manually
for (int y = 0; y < processed.getHeight(); y++) {
for (int x = 0; x < processed.getWidth(); x++) {
int rgb = processed.getRGB(x, y);
int r = (rgb >> 16) & 0xFF;
int g = (rgb >> 8) & 0xFF;
int b = rgb & 0xFF;
int gray = (r + g + b) / 3;
int grayRGB = (gray << 16) | (gray << 8) | gray;
processed.setRGB(x, y, grayRGB);
}
}
} else {
processed =
new BufferedImage(
image.getWidth(),
image.getHeight(),
BufferedImage.TYPE_INT_RGB);
Graphics2D gCol = processed.createGraphics();
gCol.drawImage(image, 0, 0, null);
gCol.dispose();
}
// 2. Add border with randomized grey gradient
int baseW = processed.getWidth() + 2 * borderPx;
int baseH = processed.getHeight() + 2 * borderPx;
boolean vertical = RANDOM.nextBoolean();
float startGrey = 0.6f + 0.3f * RANDOM.nextFloat();
float endGrey = 0.6f + 0.3f * RANDOM.nextFloat();
Color startColor =
new Color(
Math.round(startGrey * 255),
Math.round(startGrey * 255),
Math.round(startGrey * 255));
Color endColor =
new Color(
Math.round(endGrey * 255),
Math.round(endGrey * 255),
Math.round(endGrey * 255));
BufferedImage composed = new BufferedImage(baseW, baseH, processed.getType());
Graphics2D gBg = composed.createGraphics();
for (int y = 0; y < baseH; y++) {
for (int x = 0; x < baseW; x++) {
float frac = vertical ? (float) y / (baseH - 1) : (float) x / (baseW - 1);
int r =
Math.round(
startColor.getRed()
+ (endColor.getRed() - startColor.getRed()) * frac);
int g =
Math.round(
startColor.getGreen()
+ (endColor.getGreen() - startColor.getGreen())
* frac);
int b =
Math.round(
startColor.getBlue()
+ (endColor.getBlue() - startColor.getBlue())
* frac);
composed.setRGB(x, y, new Color(r, g, b).getRGB());
}
}
gBg.drawImage(processed, borderPx, borderPx, null);
gBg.dispose();
// 3. Rotate the entire composed image
double pageRotation = baseRotation;
if (baseRotation != 0 || rotateVariance != 0) {
pageRotation += (RANDOM.nextDouble() * 2 - 1) * rotateVariance;
}
BufferedImage rotated;
int w = composed.getWidth();
int h = composed.getHeight();
int rotW = w;
int rotH = h;
// Skip rotation entirely if no rotation is needed
if (pageRotation == 0) {
rotated = composed;
} else {
double radians = Math.toRadians(pageRotation);
double sin = Math.abs(Math.sin(radians));
double cos = Math.abs(Math.cos(radians));
rotW = (int) Math.floor(w * cos + h * sin);
rotH = (int) Math.floor(h * cos + w * sin);
BufferedImage rotatedBg = new BufferedImage(rotW, rotH, composed.getType());
Graphics2D gBgRot = rotatedBg.createGraphics();
for (int y = 0; y < rotH; y++) {
for (int x = 0; x < rotW; x++) {
float frac = vertical ? (float) y / (rotH - 1) : (float) x / (rotW - 1);
int r =
Math.round(
startColor.getRed()
+ (endColor.getRed() - startColor.getRed())
* frac);
int g =
Math.round(
startColor.getGreen()
+ (endColor.getGreen() - startColor.getGreen())
* frac);
int b =
Math.round(
startColor.getBlue()
+ (endColor.getBlue() - startColor.getBlue())
* frac);
rotatedBg.setRGB(x, y, new Color(r, g, b).getRGB());
}
}
gBgRot.dispose();
rotated = new BufferedImage(rotW, rotH, composed.getType());
Graphics2D g2d = rotated.createGraphics();
g2d.drawImage(rotatedBg, 0, 0, null);
AffineTransform at = new AffineTransform();
at.translate((rotW - w) / 2.0, (rotH - h) / 2.0);
at.rotate(radians, w / 2.0, h / 2.0);
g2d.setRenderingHint(
RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BICUBIC);
g2d.setRenderingHint(
RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g2d.setRenderingHint(
RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.drawImage(composed, at, null);
g2d.dispose();
}
// 4. Scale and center the rotated image to cover the original page size
PDRectangle origPageSize = document.getPage(i).getMediaBox();
float origW = origPageSize.getWidth();
float origH = origPageSize.getHeight();
float scale = Math.max(origW / rotW, origH / rotH);
float drawW = rotW * scale;
float drawH = rotH * scale;
float offsetX = (origW - drawW) / 2f;
float offsetY = (origH - drawH) / 2f;
// 5. Apply adaptive blur and edge softening
BufferedImage softened =
softenEdges(
rotated,
Math.max(10, Math.round(Math.min(rotW, rotH) * 0.02f)),
startColor,
endColor,
vertical);
BufferedImage blurred = applyGaussianBlur(softened, blur);
// 6. Adjust brightness and contrast
BufferedImage adjusted = adjustBrightnessContrast(blurred, brightness, contrast);
// 7. Add noise and yellowish effect to the content
if (yellowish) {
applyYellowishEffect(adjusted);
}
addGaussianNoise(adjusted, noise);
// 8. Write to PDF
PDPage newPage = new PDPage(new PDRectangle(origW, origH));
outputDocument.addPage(newPage);
try (PDPageContentStream contentStream =
new PDPageContentStream(outputDocument, newPage)) {
PDImageXObject pdImage =
LosslessFactory.createFromImage(outputDocument, adjusted);
contentStream.drawImage(pdImage, offsetX, offsetY, drawW, drawH);
}
}
// Save to byte array
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
outputDocument.save(outputStream);
outputDocument.close();
String outputFilename =
Filenames.toSimpleFileName(file.getOriginalFilename())
.replaceFirst("[.][^.]+$", "")
+ "_scanned.pdf";
return WebResponseUtils.bytesToWebResponse(
outputStream.toByteArray(), outputFilename, MediaType.APPLICATION_PDF);
}
}
private BufferedImage softenEdges(
BufferedImage image,
int featherRadius,
Color startColor,
Color endColor,
boolean vertical) {
int width = image.getWidth();
int height = image.getHeight();
BufferedImage output = new BufferedImage(width, height, image.getType());
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int dx = Math.min(x, width - 1 - x);
int dy = Math.min(y, height - 1 - y);
int d = Math.min(dx, dy);
float frac = vertical ? (float) y / (height - 1) : (float) x / (width - 1);
int rBg =
Math.round(
startColor.getRed()
+ (endColor.getRed() - startColor.getRed()) * frac);
int gBg =
Math.round(
startColor.getGreen()
+ (endColor.getGreen() - startColor.getGreen()) * frac);
int bBg =
Math.round(
startColor.getBlue()
+ (endColor.getBlue() - startColor.getBlue()) * frac);
int bgVal = new Color(rBg, gBg, bBg).getRGB();
int fgVal = image.getRGB(x, y);
float alpha = d < featherRadius ? (float) d / featherRadius : 1.0f;
int blended = blendColors(fgVal, bgVal, alpha);
output.setRGB(x, y, blended);
}
}
return output;
}
private int blendColors(int fg, int bg, float alpha) {
int r = Math.round(((fg >> 16) & 0xFF) * alpha + ((bg >> 16) & 0xFF) * (1 - alpha));
int g = Math.round(((fg >> 8) & 0xFF) * alpha + ((bg >> 8) & 0xFF) * (1 - alpha));
int b = Math.round((fg & 0xFF) * alpha + (bg & 0xFF) * (1 - alpha));
return (r << 16) | (g << 8) | b;
}
private BufferedImage applyGaussianBlur(BufferedImage image, double sigma) {
if (sigma <= 0) {
return image;
}
// Scale sigma based on image size to maintain consistent blur effect
double scaledSigma = sigma * Math.min(image.getWidth(), image.getHeight()) / 1000.0;
int radius = Math.max(1, (int) Math.ceil(scaledSigma * 3));
int size = 2 * radius + 1;
float[] data = new float[size * size];
double sum = 0.0;
// Generate Gaussian kernel
for (int i = -radius; i <= radius; i++) {
for (int j = -radius; j <= radius; j++) {
double xDistance = (double) i * i;
double yDistance = (double) j * j;
double g = Math.exp(-(xDistance + yDistance) / (2 * scaledSigma * scaledSigma));
data[(i + radius) * size + j + radius] = (float) g;
sum += g;
}
}
// Normalize kernel
for (int i = 0; i < data.length; i++) {
data[i] /= (float) sum;
}
// Create and apply convolution
java.awt.image.Kernel kernel = new java.awt.image.Kernel(size, size, data);
java.awt.image.ConvolveOp op =
new java.awt.image.ConvolveOp(kernel, java.awt.image.ConvolveOp.EDGE_NO_OP, null);
// Apply blur with high-quality rendering hints
BufferedImage result =
new BufferedImage(image.getWidth(), image.getHeight(), image.getType());
Graphics2D g2d = result.createGraphics();
g2d.setRenderingHint(
RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.drawImage(op.filter(image, null), 0, 0, null);
g2d.dispose();
return result;
}
private void applyYellowishEffect(BufferedImage image) {
for (int x = 0; x < image.getWidth(); x++) {
for (int y = 0; y < image.getHeight(); y++) {
int rgb = image.getRGB(x, y);
int r = (rgb >> 16) & 0xFF;
int g = (rgb >> 8) & 0xFF;
int b = rgb & 0xFF;
// Stronger yellow tint while preserving brightness
float brightness = (r + g + b) / 765.0f; // Normalize to 0-1
r = Math.min(255, (int) (r + (255 - r) * 0.18f * brightness));
g = Math.min(255, (int) (g + (255 - g) * 0.12f * brightness));
b = Math.max(0, (int) (b * (1 - 0.25f * brightness)));
image.setRGB(x, y, (r << 16) | (g << 8) | b);
}
}
}
private void addGaussianNoise(BufferedImage image, double strength) {
if (strength <= 0) return;
// Scale noise based on image size
double scaledStrength = strength * Math.min(image.getWidth(), image.getHeight()) / 1000.0;
for (int x = 0; x < image.getWidth(); x++) {
for (int y = 0; y < image.getHeight(); y++) {
int rgb = image.getRGB(x, y);
int r = (rgb >> 16) & 0xFF;
int g = (rgb >> 8) & 0xFF;
int b = rgb & 0xFF;
// Generate noise with better distribution
double noiseR = RANDOM.nextGaussian() * scaledStrength;
double noiseG = RANDOM.nextGaussian() * scaledStrength;
double noiseB = RANDOM.nextGaussian() * scaledStrength;
// Apply noise with better color preservation
r = Math.min(255, Math.max(0, r + (int) noiseR));
g = Math.min(255, Math.max(0, g + (int) noiseG));
b = Math.min(255, Math.max(0, b + (int) noiseB));
image.setRGB(x, y, (r << 16) | (g << 8) | b);
}
}
}
private BufferedImage adjustBrightnessContrast(
BufferedImage image, float brightness, float contrast) {
BufferedImage output =
new BufferedImage(image.getWidth(), image.getHeight(), image.getType());
for (int y = 0; y < image.getHeight(); y++) {
for (int x = 0; x < image.getWidth(); x++) {
int rgb = image.getRGB(x, y);
int r = (int) (((((rgb >> 16) & 0xFF) - 128) * contrast + 128) * brightness);
int g = (int) (((((rgb >> 8) & 0xFF) - 128) * contrast + 128) * brightness);
int b = (int) ((((rgb & 0xFF) - 128) * contrast + 128) * brightness);
r = Math.min(255, Math.max(0, r));
g = Math.min(255, Math.max(0, g));
b = Math.min(255, Math.max(0, b));
output.setRGB(x, y, (r << 16) | (g << 8) | b);
}
}
return output;
}
}

View File

@ -0,0 +1,311 @@
package stirling.software.SPDF.controller.api.misc;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Path2D;
import java.awt.image.*;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.graphics.image.JPEGFactory;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.apache.pdfbox.rendering.ImageType;
import org.apache.pdfbox.rendering.PDFRenderer;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.github.pixee.security.Filenames;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.common.model.api.PDFFile;
import stirling.software.common.util.PdfUtils;
import stirling.software.common.util.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/misc")
@Tag(name = "Misc", description = "Miscellaneous APIs")
public class FakeScanControllerWIP {
// TODO finish
@PostMapping(consumes = "multipart/form-data", value = "/fake-scan")
@Hidden
@Operation(
summary = "Repair a PDF file",
description =
"This endpoint repairs a given PDF file by running qpdf command. The PDF is first saved to a temporary location, repaired, read back, and then returned as a response.")
public ResponseEntity<byte[]> fakeScan(@ModelAttribute PDFFile request) throws IOException {
MultipartFile inputFile = request.getFileInput();
// Load the PDF document
PDDocument document = Loader.loadPDF(inputFile.getBytes());
PDFRenderer renderer = new PDFRenderer(document);
List<BufferedImage> images = new ArrayList<>();
// Convert each page to an image
for (int i = 0; i < document.getNumberOfPages(); i++) {
BufferedImage image = renderer.renderImageWithDPI(i, 150, ImageType.GRAY);
images.add(processImage(image));
}
document.close();
// Create a new PDF document with the processed images
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PDDocument newDocument = new PDDocument();
for (BufferedImage img : images) {
// PDPageContentStream contentStream = new PDPageContentStream(newDocument, new
// PDPage());
PDImageXObject pdImage = JPEGFactory.createFromImage(newDocument, img);
PdfUtils.addImageToDocument(newDocument, pdImage, "maintainAspectRatio", false);
}
newDocument.save(baos);
newDocument.close();
// Return the optimized PDF as a response
String outputFilename =
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
.replaceFirst("[.][^.]+$", "")
+ "_scanned.pdf";
return WebResponseUtils.boasToWebResponse(baos, outputFilename);
}
public BufferedImage processImage(BufferedImage image) {
// Rotation
image = softenEdges(image, 50);
image = rotate(image, 1);
image = applyGaussianBlur(image, 0.5);
addGaussianNoise(image, 0.5);
image = linearStretch(image);
addDustAndHairs(image, 3);
return image;
}
private BufferedImage rotate(BufferedImage image, double rotation) {
double rotationRequired = Math.toRadians(rotation);
double locationX = (double) image.getWidth() / 2;
double locationY = (double) image.getHeight() / 2;
AffineTransform tx =
AffineTransform.getRotateInstance(rotationRequired, locationX, locationY);
AffineTransformOp op = new AffineTransformOp(tx, AffineTransformOp.TYPE_BICUBIC);
return op.filter(image, null);
}
private BufferedImage applyGaussianBlur(BufferedImage image, double sigma) {
int radius = 3; // Fixed radius size for simplicity
int size = 2 * radius + 1;
float[] data = new float[size * size];
double sum = 0.0;
for (int i = -radius; i <= radius; i++) {
for (int j = -radius; j <= radius; j++) {
double xDistance = (double) i * i;
double yDistance = (double) j * j;
double g = Math.exp(-(xDistance + yDistance) / (2 * sigma * sigma));
data[(i + radius) * size + j + radius] = (float) g;
sum += g;
}
}
// Normalize the kernel
for (int i = 0; i < data.length; i++) {
if (sum != 0) data[i] /= sum;
}
Kernel kernel = new Kernel(size, size, data);
BufferedImageOp op = new ConvolveOp(kernel, ConvolveOp.EDGE_NO_OP, null);
return op.filter(image, null);
}
public BufferedImage softenEdges(BufferedImage image, int featherRadius) {
int width = image.getWidth();
int height = image.getHeight();
BufferedImage output = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = output.createGraphics();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g2.setRenderingHint(
RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
g2.drawImage(image, 0, 0, null);
g2.setComposite(AlphaComposite.DstIn);
// Top edge
g2.setPaint(
new GradientPaint(
0,
0,
new Color(0, 0, 0, 1f),
0,
featherRadius * 2f,
new Color(0, 0, 0, 0f)));
g2.fillRect(0, 0, width, featherRadius);
// Bottom edge
g2.setPaint(
new GradientPaint(
0,
height - featherRadius * 2f,
new Color(0, 0, 0, 0f),
0,
height,
new Color(0, 0, 0, 1f)));
g2.fillRect(0, height - featherRadius, width, featherRadius);
// Left edge
g2.setPaint(
new GradientPaint(
0,
0,
new Color(0, 0, 0, 1f),
featherRadius * 2f,
0,
new Color(0, 0, 0, 0f)));
g2.fillRect(0, 0, featherRadius, height);
// Right edge
g2.setPaint(
new GradientPaint(
width - featherRadius * 2f,
0,
new Color(0, 0, 0, 0f),
width,
0,
new Color(0, 0, 0, 1f)));
g2.fillRect(width - featherRadius, 0, featherRadius, height);
g2.dispose();
return output;
}
private void addDustAndHairs(BufferedImage image, float intensity) {
int width = image.getWidth();
int height = image.getHeight();
Graphics2D g2d = image.createGraphics();
Random random = new SecureRandom();
// Set rendering hints for better quality
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// Calculate the number of artifacts based on intensity
int numSpots = (int) (intensity * 10);
int numHairs = (int) (intensity * 20);
// Add spots with more variable sizes
g2d.setColor(new Color(100, 100, 100, 50)); // Semi-transparent gray
for (int i = 0; i < numSpots; i++) {
int x = random.nextInt(width);
int y = random.nextInt(height);
int ovalSize = 1 + random.nextInt(3); // Base size + variable component
if (random.nextFloat() > 0.9) {
// 10% chance to get a larger spot
ovalSize += random.nextInt(3);
}
g2d.fill(new Ellipse2D.Double(x, y, ovalSize, ovalSize));
}
// Add hairs
g2d.setStroke(new BasicStroke(0.5f)); // Thin stroke for hairs
g2d.setColor(new Color(80, 80, 80, 40)); // Slightly lighter and more transparent
for (int i = 0; i < numHairs; i++) {
int x1 = random.nextInt(width);
int y1 = random.nextInt(height);
int x2 = x1 + random.nextInt(20) - 10; // Random length and direction
int y2 = y1 + random.nextInt(20) - 10;
Path2D.Double hair = new Path2D.Double();
hair.moveTo(x1, y1);
hair.curveTo(x1, y1, (double) (x1 + x2) / 2, (double) (y1 + y2) / 2, x2, y2);
g2d.draw(hair);
}
g2d.dispose();
}
private void addGaussianNoise(BufferedImage image, double strength) {
Random rand = new SecureRandom();
int width = image.getWidth();
int height = image.getHeight();
for (int i = 0; i < width; i++) {
for (int j = 0; j < height; j++) {
int rgba = image.getRGB(i, j);
int alpha = (rgba >> 24) & 0xff;
int red = (rgba >> 16) & 0xff;
int green = (rgba >> 8) & 0xff;
int blue = rgba & 0xff;
// Apply Gaussian noise
red = (int) (red + rand.nextGaussian() * strength);
green = (int) (green + rand.nextGaussian() * strength);
blue = (int) (blue + rand.nextGaussian() * strength);
// Clamping values to the 0-255 range
red = Math.min(Math.max(0, red), 255);
green = Math.min(Math.max(0, green), 255);
blue = Math.min(Math.max(0, blue), 255);
image.setRGB(i, j, (alpha << 24) | (red << 16) | (green << 8) | blue);
}
}
}
public BufferedImage linearStretch(BufferedImage image) {
int width = image.getWidth();
int height = image.getHeight();
int min = 255;
int max = 0;
// First pass: find the min and max grayscale values
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int rgb = image.getRGB(x, y);
int gray =
(int)
(((rgb >> 16) & 0xff) * 0.299
+ ((rgb >> 8) & 0xff) * 0.587
+ (rgb & 0xff) * 0.114); // Convert to grayscale
if (gray < min) min = gray;
if (gray > max) max = gray;
}
}
// Second pass: stretch the histogram
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int rgb = image.getRGB(x, y);
int alpha = (rgb >> 24) & 0xff;
int red = (rgb >> 16) & 0xff;
int green = (rgb >> 8) & 0xff;
int blue = rgb & 0xff;
// Apply linear stretch to each channel
red = (int) (((red - min) / (float) (max - min)) * 255);
green = (int) (((green - min) / (float) (max - min)) * 255);
blue = (int) (((blue - min) / (float) (max - min)) * 255);
// Set new RGB value maintaining the alpha channel
rgb = (alpha << 24) | (red << 16) | (green << 8) | blue;
image.setRGB(x, y, rgb);
}
}
return image;
}
}

View File

@ -1,126 +0,0 @@
package stirling.software.SPDF.model.api.misc;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode
public class FakeScanRequest {
public enum Quality {
low,
medium,
high
}
public enum Rotation {
none,
slight,
moderate,
severe
}
public enum Colorspace {
grayscale,
color
}
@Schema(
description = "PDF file to process",
requiredMode = Schema.RequiredMode.REQUIRED,
type = "string",
format = "binary")
@NotNull(message = "File input is required")
private MultipartFile fileInput;
@Schema(description = "Scan quality preset", example = "high")
@NotNull(message = "Quality is required")
private Quality quality = Quality.high;
@Schema(description = "Rotation preset", example = "none")
@NotNull(message = "Rotation is required")
private Rotation rotation = Rotation.slight;
@Schema(description = "Colorspace for output image", example = "grayscale")
private Colorspace colorspace = Colorspace.grayscale;
@Schema(description = "Border thickness in pixels", example = "20")
private int border = 20;
@Schema(description = "Base rotation in degrees", example = "0")
private int rotate = 0;
@Schema(description = "Random rotation variance in degrees", example = "2")
private int rotateVariance = 2;
@Schema(description = "Brightness multiplier (1.0 = no change)", example = "1.0")
private float brightness = 1.0f;
@Schema(description = "Contrast multiplier (1.0 = no change)", example = "1.0")
private float contrast = 1.0f;
@Schema(description = "Blur amount (0 = none, higher = more blur)", example = "1.0")
private float blur = 1.0f;
@Schema(description = "Noise amount (0 = none, higher = more noise)", example = "8.0")
private float noise = 8.0f;
@Schema(description = "Simulate yellowed paper", example = "false")
private boolean yellowish = false;
@Schema(description = "Rendering resolution in DPI", example = "300")
private int resolution = 300;
@Schema(description = "Whether advanced settings are enabled", example = "false")
private boolean advancedEnabled = false;
public boolean isAdvancedEnabled() {
return advancedEnabled;
}
public int getQualityValue() {
return switch (quality) {
case low -> 30;
case medium -> 60;
case high -> 100;
};
}
public int getRotationValue() {
return switch (rotation) {
case none -> 0;
case slight -> 2;
case moderate -> 5;
case severe -> 8;
};
}
public void applyHighQualityPreset() {
this.blur = 0.1f;
this.noise = 1.0f;
this.brightness = 1.02f;
this.contrast = 1.05f;
this.resolution = 600;
}
public void applyMediumQualityPreset() {
this.blur = 0.5f;
this.noise = 3.0f;
this.brightness = 1.05f;
this.contrast = 1.1f;
this.resolution = 300;
}
public void applyLowQualityPreset() {
this.blur = 1.0f;
this.noise = 5.0f;
this.brightness = 1.1f;
this.contrast = 1.2f;
this.resolution = 150;
}
}

View File

@ -1569,38 +1569,3 @@ cookieBanner.preferencesModal.necessary.description=These cookies are essential
cookieBanner.preferencesModal.analytics.title=Analytics cookieBanner.preferencesModal.analytics.title=Analytics
cookieBanner.preferencesModal.analytics.description=These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assured—Stirling PDF cannot and will never track the content of the documents you work with. cookieBanner.preferencesModal.analytics.description=These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assured—Stirling PDF cannot and will never track the content of the documents you work with.
#fakeScan
fakeScan.title=Fake Scan
fakeScan.header=Fake Scan
fakeScan.description=Create a PDF that looks like it was scanned
fakeScan.selectPDF=Select PDF:
fakeScan.quality=Scan Quality
fakeScan.quality.low=Low
fakeScan.quality.medium=Medium
fakeScan.quality.high=High
fakeScan.rotation=Rotation Angle
fakeScan.rotation.none=None
fakeScan.rotation.slight=Slight
fakeScan.rotation.moderate=Moderate
fakeScan.rotation.severe=Severe
fakeScan.submit=Create Fake Scan
#home.fakeScan
home.fakeScan.title=Fake Scan
home.fakeScan.desc=Create a PDF that looks like it was scanned
fakeScan.tags=scan,simulate,realistic,convert
# FakeScan advanced settings (frontend)
fakeScan.advancedSettings=Enable Advanced Scan Settings
fakeScan.colorspace=Colorspace
fakeScan.colorspace.grayscale=Grayscale
fakeScan.colorspace.color=Color
fakeScan.border=Border (px)
fakeScan.rotate=Base Rotation (degrees)
fakeScan.rotateVariance=Rotation Variance (degrees)
fakeScan.brightness=Brightness
fakeScan.contrast=Contrast
fakeScan.blur=Blur
fakeScan.noise=Noise
fakeScan.yellowish=Yellowish (simulate old paper)
fakeScan.resolution=Resolution (DPI)

View File

@ -112,7 +112,7 @@ lang.spa=Spagnolo
lang.spa_old=Spagnolo (antico) lang.spa_old=Spagnolo (antico)
lang.sqi=Albanese lang.sqi=Albanese
lang.srp=Serbo lang.srp=Serbo
lang.srp_latn=Serbo (latino) lang.srp_latn=Serbian (Latin)
lang.sun=Sundanese lang.sun=Sundanese
lang.swa=Swahili lang.swa=Swahili
lang.swe=Svedese lang.swe=Svedese

View File

@ -4,7 +4,7 @@
<div th:replace="~{fragments/languageEntry :: languageEntry ('ca_CA', 'Català')}"></div> <div th:replace="~{fragments/languageEntry :: languageEntry ('ca_CA', 'Català')}"></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('zh_CN', '简体中文')}"></div> <div th:replace="~{fragments/languageEntry :: languageEntry ('zh_CN', '简体中文')}"></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('zh_TW', '繁體中文')}"></div> <div th:replace="~{fragments/languageEntry :: languageEntry ('zh_TW', '繁體中文')}"></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('bo_CN', 'བོད་ཡིག')}"></div> <div th:replace="~{fragments/languageEntry :: languageEntry ('zh_BO', 'བོད་ཡིག')}"></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('az_AZ', 'Azərbaycan Dili')}"></div> <div th:replace="~{fragments/languageEntry :: languageEntry ('az_AZ', 'Azərbaycan Dili')}"></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('da_DK', 'Dansk')}"></div> <div th:replace="~{fragments/languageEntry :: languageEntry ('da_DK', 'Dansk')}"></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('de_DE', 'Deutsch')}"></div> <div th:replace="~{fragments/languageEntry :: languageEntry ('de_DE', 'Deutsch')}"></div>

View File

@ -264,7 +264,6 @@
<div <div
th:replace="~{fragments/navbarEntry :: navbarEntry('show-javascript', 'javascript', 'home.showJS.title', 'home.showJS.desc', 'showJS.tags', 'advance')}"> th:replace="~{fragments/navbarEntry :: navbarEntry('show-javascript', 'javascript', 'home.showJS.title', 'home.showJS.desc', 'showJS.tags', 'advance')}">
</div> </div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry('fake-scan', 'scanner', 'fakeScan.title', 'fakeScan.description', 'fakeScan.tags', 'advance')}"></div>
<div <div
th:replace="~{fragments/navbarEntryCustom :: navbarEntry('split-by-size-or-count', '/images/split-size.svg#icon-split-size', 'home.autoSizeSplitPDF.title', 'home.autoSizeSplitPDF.desc', 'autoSizeSplitPDF.tags', 'advance')}"> th:replace="~{fragments/navbarEntryCustom :: navbarEntry('split-by-size-or-count', '/images/split-size.svg#icon-split-size', 'home.autoSizeSplitPDF.title', 'home.autoSizeSplitPDF.desc', 'autoSizeSplitPDF.tags', 'advance')}">
</div> </div>

View File

@ -12,85 +12,12 @@
<br><br> <br><br>
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-6 bg-card"> <div class="col-md-6">
<div class="tool-header"> <h2 th:text="#{fakeScan.header}"></h2>
<span class="material-symbols-rounded tool-header-icon advance">scanner</span> <form method="post" enctype="multipart/form-data" th:action="@{'/api/v1/misc/fake-scan'}">
<span class="tool-header-text" th:text="#{fakeScan.title}"></span>
</div>
<form method="post" enctype="multipart/form-data" id="uploadForm" th:action="@{'/api/v1/misc/fake-scan'}">
<input type="hidden" name="advancedEnabled" id="advancedEnabled" value="false">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
<br> <br>
<div class="mb-3"> <button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{fakeScan.submit}"></button>
<label for="quality" class="form-label" th:text="#{fakeScan.quality}"></label>
<select class="form-select" id="quality" name="quality">
<option value="low" th:text="#{fakeScan.quality.low}"></option>
<option value="medium" th:text="#{fakeScan.quality.medium}"></option>
<option value="high" th:text="#{fakeScan.quality.high}" selected></option>
</select>
</div>
<div class="mb-3">
<label for="rotation" class="form-label" th:text="#{fakeScan.rotation}"></label>
<select class="form-select" id="rotation" name="rotation">
<option value="none" th:text="#{fakeScan.rotation.none}"></option>
<option value="slight" th:text="#{fakeScan.rotation.slight}" selected></option>
<option value="moderate" th:text="#{fakeScan.rotation.moderate}"></option>
<option value="severe" th:text="#{fakeScan.rotation.severe}"></option>
</select>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="advancedSettingsToggle">
<label class="form-check-label" for="advancedSettingsToggle" th:text="#{fakeScan.advancedSettings}"></label>
</div>
<div id="advancedSettings" style="display:none; border:1px solid #eee; padding:1em; border-radius:8px; margin-bottom:1em;">
<div class="mb-3">
<label for="colorspace" class="form-label" th:text="#{fakeScan.colorspace}"></label>
<select class="form-select" id="colorspace" name="colorspace">
<option value="grayscale" th:text="#{fakeScan.colorspace.grayscale}" selected></option>
<option value="color" th:text="#{fakeScan.colorspace.color}"></option>
</select>
</div>
<div class="mb-3">
<label for="border" class="form-label" th:text="#{fakeScan.border}"></label>
<input type="number" class="form-control" id="border" name="border" min="0" max="100" value="20">
</div>
<div class="mb-3">
<label for="rotate" class="form-label" th:text="#{fakeScan.rotate}"></label>
<input type="number" class="form-control" id="rotate" name="rotate" min="-15" max="15" value="0">
</div>
<div class="mb-3">
<label for="rotateVariance" class="form-label" th:text="#{fakeScan.rotateVariance}"></label>
<input type="number" class="form-control" id="rotateVariance" name="rotateVariance" min="0" max="10" value="2">
</div>
<div class="mb-3">
<label for="brightness" class="form-label" th:text="#{fakeScan.brightness}"></label>
<input type="range" class="form-range" id="brightness" name="brightness" min="0.5" max="1.5" step="0.01" value="1.0">
</div>
<div class="mb-3">
<label for="contrast" class="form-label" th:text="#{fakeScan.contrast}"></label>
<input type="range" class="form-range" id="contrast" name="contrast" min="0.5" max="1.5" step="0.01" value="1.0">
</div>
<div class="mb-3">
<label for="blur" class="form-label" th:text="#{fakeScan.blur}"></label>
<input type="range" class="form-range" id="blur" name="blur" min="0" max="5" step="0.1" value="1.0">
</div>
<div class="mb-3">
<label for="noise" class="form-label" th:text="#{fakeScan.noise}"></label>
<input type="range" class="form-range" id="noise" name="noise" min="0" max="20" step="0.1" value="8.0">
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="yellowish" name="yellowish">
<label class="form-check-label" for="yellowish" th:text="#{fakeScan.yellowish}"></label>
</div>
<div class="mb-3">
<label for="resolution" class="form-label" th:text="#{fakeScan.resolution}"></label>
<input type="number" class="form-control" id="resolution" name="resolution" min="72" max="600" value="300">
</div>
</div>
<div class="mb-3 text-left">
<button type="submit" class="btn btn-primary" th:text="#{fakeScan.submit}" id="submitBtn"></button>
</div>
</form> </form>
</div> </div>
</div> </div>
@ -98,66 +25,5 @@
</div> </div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block> <th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div> </div>
<script th:src="@{'/js/fetch-utils.js'}"></script>
<script th:inline="javascript">
// Show/hide advanced settings
document.getElementById('advancedSettingsToggle').addEventListener('change', function() {
document.getElementById('advancedSettings').style.display = this.checked ? 'block' : 'none';
});
// Form submission handling
const form = document.getElementById('uploadForm');
if (form) {
form.addEventListener('submit', function(e) {
// If advanced settings are not enabled, remove advanced fields
if (!document.getElementById('advancedSettingsToggle').checked) {
const formData = new FormData(form);
formData.delete('colorspace');
formData.delete('border');
formData.delete('rotate');
formData.delete('rotateVariance');
formData.delete('brightness');
formData.delete('contrast');
formData.delete('blur');
formData.delete('noise');
formData.delete('yellowish');
formData.delete('resolution');
}
});
}
// Initialize advanced settings state
const advancedSettingsToggle = document.getElementById('advancedSettingsToggle');
const advancedSettings = document.getElementById('advancedSettings');
if (advancedSettingsToggle && advancedSettings) {
// Helper: enable/disable all fields in advanced settings
function setAdvancedFieldsEnabled(enabled) {
const fields = advancedSettings.querySelectorAll('input, select');
fields.forEach(field => {
field.disabled = !enabled;
// If enabling and value is empty, set to default
if (enabled && (field.value === '' || field.value == null)) {
if (field.type === 'number' || field.type === 'range') {
field.value = field.defaultValue;
} else if (field.type === 'checkbox') {
field.checked = field.defaultChecked;
} else if (field.tagName === 'SELECT') {
field.value = field.querySelector('option[selected]')?.value || field.options[0].value;
}
}
});
}
// Set initial state
setAdvancedFieldsEnabled(advancedSettingsToggle.checked);
advancedSettings.style.display = advancedSettingsToggle.checked ? 'block' : 'none';
document.getElementById('advancedEnabled').value = advancedSettingsToggle.checked ? 'true' : 'false';
// Add change listener
advancedSettingsToggle.addEventListener('change', function() {
advancedSettings.style.display = this.checked ? 'block' : 'none';
setAdvancedFieldsEnabled(this.checked);
document.getElementById('advancedEnabled').value = this.checked ? 'true' : 'false';
});
}
</script>
</body> </body>
</html> </html>