diff --git a/.github/config/.files.yaml b/.github/config/.files.yaml index a5d8410f3..78f461683 100644 --- a/.github/config/.files.yaml +++ b/.github/config/.files.yaml @@ -29,3 +29,10 @@ project: &project - settings.gradle - frontend/** - docker/** + - testing/** + +frontend: &frontend + - frontend/** + - .github/workflows/testdriver.yml + - testing/** + - docker/** diff --git a/.github/scripts/requirements_dev.in b/.github/scripts/requirements_dev.in new file mode 100644 index 000000000..a8732d927 --- /dev/null +++ b/.github/scripts/requirements_dev.in @@ -0,0 +1,8 @@ +pip +setuptools +WeasyPrint +pdf2image +pillow +unoserver +opencv-python-headless +pre-commit diff --git a/.github/scripts/requirements_dev.txt b/.github/scripts/requirements_dev.txt new file mode 100644 index 000000000..9df13c7ae --- /dev/null +++ b/.github/scripts/requirements_dev.txt @@ -0,0 +1,638 @@ +# +# This file is autogenerated by pip-compile with Python 3.12 +# by the following command: +# +# pip-compile --allow-unsafe --generate-hashes --output-file='.github\scripts\requirements_dev.txt' --strip-extras '.github\scripts\requirements_dev.in' +# +brotli==1.1.0 \ + --hash=sha256:03d20af184290887bdea3f0f78c4f737d126c74dc2f3ccadf07e54ceca3bf208 \ + --hash=sha256:0541e747cce78e24ea12d69176f6a7ddb690e62c425e01d31cc065e69ce55b48 \ + --hash=sha256:069a121ac97412d1fe506da790b3e69f52254b9df4eb665cd42460c837193354 \ + --hash=sha256:0737ddb3068957cf1b054899b0883830bb1fec522ec76b1098f9b6e0f02d9419 \ + --hash=sha256:0b63b949ff929fbc2d6d3ce0e924c9b93c9785d877a21a1b678877ffbbc4423a \ + --hash=sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128 \ + --hash=sha256:11d00ed0a83fa22d29bc6b64ef636c4552ebafcef57154b4ddd132f5638fbd1c \ + --hash=sha256:141bd4d93984070e097521ed07e2575b46f817d08f9fa42b16b9b5f27b5ac088 \ + --hash=sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9 \ + --hash=sha256:1ab4fbee0b2d9098c74f3057b2bc055a8bd92ccf02f65944a241b4349229185a \ + --hash=sha256:1ae56aca0402a0f9a3431cddda62ad71666ca9d4dc3a10a142b9dce2e3c0cda3 \ + --hash=sha256:1b2c248cd517c222d89e74669a4adfa5577e06ab68771a529060cf5a156e9757 \ + --hash=sha256:1e9a65b5736232e7a7f91ff3d02277f11d339bf34099a56cdab6a8b3410a02b2 \ + --hash=sha256:224e57f6eac61cc449f498cc5f0e1725ba2071a3d4f48d5d9dffba42db196438 \ + --hash=sha256:22fc2a8549ffe699bfba2256ab2ed0421a7b8fadff114a3d201794e45a9ff578 \ + --hash=sha256:23032ae55523cc7bccb4f6a0bf368cd25ad9bcdcc1990b64a647e7bbcce9cb5b \ + --hash=sha256:2333e30a5e00fe0fe55903c8832e08ee9c3b1382aacf4db26664a16528d51b4b \ + --hash=sha256:2954c1c23f81c2eaf0b0717d9380bd348578a94161a65b3a2afc62c86467dd68 \ + --hash=sha256:2a24c50840d89ded6c9a8fdc7b6ed3692ed4e86f1c4a4a938e1e92def92933e0 \ + --hash=sha256:2de9d02f5bda03d27ede52e8cfe7b865b066fa49258cbab568720aa5be80a47d \ + --hash=sha256:2feb1d960f760a575dbc5ab3b1c00504b24caaf6986e2dc2b01c09c87866a943 \ + --hash=sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd \ + --hash=sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409 \ + --hash=sha256:32d95b80260d79926f5fab3c41701dbb818fde1c9da590e77e571eefd14abe28 \ + --hash=sha256:38025d9f30cf4634f8309c6874ef871b841eb3c347e90b0851f63d1ded5212da \ + --hash=sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50 \ + --hash=sha256:3c0ef38c7a7014ffac184db9e04debe495d317cc9c6fb10071f7fefd93100a4f \ + --hash=sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0 \ + --hash=sha256:3ee8a80d67a4334482d9712b8e83ca6b1d9bc7e351931252ebef5d8f7335a547 \ + --hash=sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180 \ + --hash=sha256:43395e90523f9c23a3d5bdf004733246fba087f2948f87ab28015f12359ca6a0 \ + --hash=sha256:43ce1b9935bfa1ede40028054d7f48b5469cd02733a365eec8a329ffd342915d \ + --hash=sha256:4410f84b33374409552ac9b6903507cdb31cd30d2501fc5ca13d18f73548444a \ + --hash=sha256:494994f807ba0b92092a163a0a283961369a65f6cbe01e8891132b7a320e61eb \ + --hash=sha256:4d4a848d1837973bf0f4b5e54e3bec977d99be36a7895c61abb659301b02c112 \ + --hash=sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc \ + --hash=sha256:4f3607b129417e111e30637af1b56f24f7a49e64763253bbc275c75fa887d4b2 \ + --hash=sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265 \ + --hash=sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327 \ + --hash=sha256:587ca6d3cef6e4e868102672d3bd9dc9698c309ba56d41c2b9c85bbb903cdb95 \ + --hash=sha256:58d4b711689366d4a03ac7957ab8c28890415e267f9b6589969e74b6e42225ec \ + --hash=sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd \ + --hash=sha256:5dab0844f2cf82be357a0eb11a9087f70c5430b2c241493fc122bb6f2bb0917c \ + --hash=sha256:5e55da2c8724191e5b557f8e18943b1b4839b8efc3ef60d65985bcf6f587dd38 \ + --hash=sha256:5eeb539606f18a0b232d4ba45adccde4125592f3f636a6182b4a8a436548b914 \ + --hash=sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0 \ + --hash=sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a \ + --hash=sha256:6172447e1b368dcbc458925e5ddaf9113477b0ed542df258d84fa28fc45ceea7 \ + --hash=sha256:6967ced6730aed543b8673008b5a391c3b1076d834ca438bbd70635c73775368 \ + --hash=sha256:6974f52a02321b36847cd19d1b8e381bf39939c21efd6ee2fc13a28b0d99348c \ + --hash=sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0 \ + --hash=sha256:6c6e0c425f22c1c719c42670d561ad682f7bfeeef918edea971a79ac5252437f \ + --hash=sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451 \ + --hash=sha256:7905193081db9bfa73b1219140b3d315831cbff0d8941f22da695832f0dd188f \ + --hash=sha256:7bc37c4d6b87fb1017ea28c9508b36bbcb0c3d18b4260fcdf08b200c74a6aee8 \ + --hash=sha256:7c4855522edb2e6ae7fdb58e07c3ba9111e7621a8956f481c68d5d979c93032e \ + --hash=sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248 \ + --hash=sha256:7eedaa5d036d9336c95915035fb57422054014ebdeb6f3b42eac809928e40d0c \ + --hash=sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91 \ + --hash=sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724 \ + --hash=sha256:832436e59afb93e1836081a20f324cb185836c617659b07b129141a8426973c7 \ + --hash=sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966 \ + --hash=sha256:87a3044c3a35055527ac75e419dfa9f4f3667a1e887ee80360589eb8c90aabb9 \ + --hash=sha256:890b5a14ce214389b2cc36ce82f3093f96f4cc730c1cffdbefff77a7c71f2a97 \ + --hash=sha256:89f4988c7203739d48c6f806f1e87a1d96e0806d44f0fba61dba81392c9e474d \ + --hash=sha256:8bf32b98b75c13ec7cf774164172683d6e7891088f6316e54425fde1efc276d5 \ + --hash=sha256:8dadd1314583ec0bf2d1379f7008ad627cd6336625d6679cf2f8e67081b83acf \ + --hash=sha256:901032ff242d479a0efa956d853d16875d42157f98951c0230f69e69f9c09bac \ + --hash=sha256:9011560a466d2eb3f5a6e4929cf4a09be405c64154e12df0dd72713f6500e32b \ + --hash=sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951 \ + --hash=sha256:919e32f147ae93a09fe064d77d5ebf4e35502a8df75c29fb05788528e330fe74 \ + --hash=sha256:91d7cc2a76b5567591d12c01f019dd7afce6ba8cba6571187e21e2fc418ae648 \ + --hash=sha256:929811df5462e182b13920da56c6e0284af407d1de637d8e536c5cd00a7daf60 \ + --hash=sha256:949f3b7c29912693cee0afcf09acd6ebc04c57af949d9bf77d6101ebb61e388c \ + --hash=sha256:a090ca607cbb6a34b0391776f0cb48062081f5f60ddcce5d11838e67a01928d1 \ + --hash=sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8 \ + --hash=sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d \ + --hash=sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc \ + --hash=sha256:a469274ad18dc0e4d316eefa616d1d0c2ff9da369af19fa6f3daa4f09671fd61 \ + --hash=sha256:a599669fd7c47233438a56936988a2478685e74854088ef5293802123b5b2460 \ + --hash=sha256:a743e5a28af5f70f9c080380a5f908d4d21d40e8f0e0c8901604d15cfa9ba751 \ + --hash=sha256:a77def80806c421b4b0af06f45d65a136e7ac0bdca3c09d9e2ea4e515367c7e9 \ + --hash=sha256:a7e53012d2853a07a4a79c00643832161a910674a893d296c9f1259859a289d2 \ + --hash=sha256:a93dde851926f4f2678e704fadeb39e16c35d8baebd5252c9fd94ce8ce68c4a0 \ + --hash=sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1 \ + --hash=sha256:ae15b066e5ad21366600ebec29a7ccbc86812ed267e4b28e860b8ca16a2bc474 \ + --hash=sha256:aea440a510e14e818e67bfc4027880e2fb500c2ccb20ab21c7a7c8b5b4703d75 \ + --hash=sha256:af6fa6817889314555aede9a919612b23739395ce767fe7fcbea9a80bf140fe5 \ + --hash=sha256:b760c65308ff1e462f65d69c12e4ae085cff3b332d894637f6273a12a482d09f \ + --hash=sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2 \ + --hash=sha256:c247dd99d39e0338a604f8c2b3bc7061d5c2e9e2ac7ba9cc1be5a69cb6cd832f \ + --hash=sha256:c5529b34c1c9d937168297f2c1fde7ebe9ebdd5e121297ff9c043bdb2ae3d6fb \ + --hash=sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6 \ + --hash=sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9 \ + --hash=sha256:ca63e1890ede90b2e4454f9a65135a4d387a4585ff8282bb72964fab893f2111 \ + --hash=sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2 \ + --hash=sha256:cb1dac1770878ade83f2ccdf7d25e494f05c9165f5246b46a621cc849341dc01 \ + --hash=sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467 \ + --hash=sha256:cdbc1fc1bc0bff1cef838eafe581b55bfbffaed4ed0318b724d0b71d4d377619 \ + --hash=sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf \ + --hash=sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408 \ + --hash=sha256:d143fd47fad1db3d7c27a1b1d66162e855b5d50a89666af46e1679c496e8e579 \ + --hash=sha256:d192f0f30804e55db0d0e0a35d83a9fead0e9a359a9ed0285dbacea60cc10a84 \ + --hash=sha256:d2b35ca2c7f81d173d2fadc2f4f31e88cc5f7a39ae5b6db5513cf3383b0e0ec7 \ + --hash=sha256:d342778ef319e1026af243ed0a07c97acf3bad33b9f29e7ae6a1f68fd083e90c \ + --hash=sha256:d487f5432bf35b60ed625d7e1b448e2dc855422e87469e3f450aa5552b0eb284 \ + --hash=sha256:d7702622a8b40c49bffb46e1e3ba2e81268d5c04a34f460978c6b5517a34dd52 \ + --hash=sha256:db85ecf4e609a48f4b29055f1e144231b90edc90af7481aa731ba2d059226b1b \ + --hash=sha256:de6551e370ef19f8de1807d0a9aa2cdfdce2e85ce88b122fe9f6b2b076837e59 \ + --hash=sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752 \ + --hash=sha256:e4fe605b917c70283db7dfe5ada75e04561479075761a0b3866c081d035b01c1 \ + --hash=sha256:e6a904cb26bfefc2f0a6f240bdf5233be78cd2488900a2f846f3c3ac8489ab80 \ + --hash=sha256:e79e6520141d792237c70bcd7a3b122d00f2613769ae0cb61c52e89fd3443839 \ + --hash=sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0 \ + --hash=sha256:e93dfc1a1165e385cc8239fab7c036fb2cd8093728cbd85097b284d7b99249a2 \ + --hash=sha256:efa8b278894b14d6da122a72fefcebc28445f2d3f880ac59d46c90f4c13be9a3 \ + --hash=sha256:f0d8a7a6b5983c2496e364b969f0e526647a06b075d034f3297dc66f3b360c64 \ + --hash=sha256:f0db75f47be8b8abc8d9e31bc7aad0547ca26f24a54e6fd10231d623f183d089 \ + --hash=sha256:f296c40e23065d0d6650c4aefe7470d2a25fffda489bcc3eb66083f3ac9f6643 \ + --hash=sha256:f31859074d57b4639318523d6ffdca586ace54271a73ad23ad021acd807eb14b \ + --hash=sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e \ + --hash=sha256:f733d788519c7e3e71f0855c96618720f5d3d60c3cb829d8bbb722dddce37985 \ + --hash=sha256:fce1473f3ccc4187f75b4690cfc922628aed4d3dd013d047f95a9b3919a86596 \ + --hash=sha256:fd5f17ff8f14003595ab414e45fce13d073e0762394f957182e69035c9f3d7c2 \ + --hash=sha256:fdc3ff3bfccdc6b9cc7c342c03aa2400683f0cb891d46e94b64a197910dc4064 + # via fonttools +cffi==1.17.1 \ + --hash=sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8 \ + --hash=sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2 \ + --hash=sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1 \ + --hash=sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15 \ + --hash=sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36 \ + --hash=sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824 \ + --hash=sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8 \ + --hash=sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36 \ + --hash=sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17 \ + --hash=sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf \ + --hash=sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc \ + --hash=sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3 \ + --hash=sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed \ + --hash=sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702 \ + --hash=sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1 \ + --hash=sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8 \ + --hash=sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903 \ + --hash=sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6 \ + --hash=sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d \ + --hash=sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b \ + --hash=sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e \ + --hash=sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be \ + --hash=sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c \ + --hash=sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683 \ + --hash=sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9 \ + --hash=sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c \ + --hash=sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8 \ + --hash=sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1 \ + --hash=sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4 \ + --hash=sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655 \ + --hash=sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67 \ + --hash=sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595 \ + --hash=sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0 \ + --hash=sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65 \ + --hash=sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41 \ + --hash=sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6 \ + --hash=sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401 \ + --hash=sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6 \ + --hash=sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3 \ + --hash=sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16 \ + --hash=sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93 \ + --hash=sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e \ + --hash=sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4 \ + --hash=sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964 \ + --hash=sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c \ + --hash=sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576 \ + --hash=sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0 \ + --hash=sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3 \ + --hash=sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662 \ + --hash=sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3 \ + --hash=sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff \ + --hash=sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5 \ + --hash=sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd \ + --hash=sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f \ + --hash=sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5 \ + --hash=sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14 \ + --hash=sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d \ + --hash=sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9 \ + --hash=sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7 \ + --hash=sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382 \ + --hash=sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a \ + --hash=sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e \ + --hash=sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a \ + --hash=sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4 \ + --hash=sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99 \ + --hash=sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87 \ + --hash=sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b + # via weasyprint +cfgv==3.4.0 \ + --hash=sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9 \ + --hash=sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560 + # via pre-commit +cssselect2==0.8.0 \ + --hash=sha256:46fc70ebc41ced7a32cd42d58b1884d72ade23d21e5a4eaaf022401c13f0e76e \ + --hash=sha256:7674ffb954a3b46162392aee2a3a0aedb2e14ecf99fcc28644900f4e6e3e9d3a + # via weasyprint +distlib==0.4.0 \ + --hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \ + --hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d + # via virtualenv +filelock==3.18.0 \ + --hash=sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2 \ + --hash=sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de + # via virtualenv +fonttools==4.59.0 \ + --hash=sha256:052444a5d0151878e87e3e512a1aa1a0ab35ee4c28afde0a778e23b0ace4a7de \ + --hash=sha256:169b99a2553a227f7b5fea8d9ecd673aa258617f466b2abc6091fe4512a0dcd0 \ + --hash=sha256:209b75943d158f610b78320eacb5539aa9e920bee2c775445b2846c65d20e19d \ + --hash=sha256:21e606b2d38fed938dde871c5736822dd6bda7a4631b92e509a1f5cd1b90c5df \ + --hash=sha256:241313683afd3baacb32a6bd124d0bce7404bc5280e12e291bae1b9bba28711d \ + --hash=sha256:26731739daa23b872643f0e4072d5939960237d540c35c14e6a06d47d71ca8fe \ + --hash=sha256:2e7cf8044ce2598bb87e44ba1d2c6e45d7a8decf56055b92906dc53f67c76d64 \ + --hash=sha256:31003b6a10f70742a63126b80863ab48175fb8272a18ca0846c0482968f0588e \ + --hash=sha256:332bfe685d1ac58ca8d62b8d6c71c2e52a6c64bc218dc8f7825c9ea51385aa01 \ + --hash=sha256:37c377f7cb2ab2eca8a0b319c68146d34a339792f9420fca6cd49cf28d370705 \ + --hash=sha256:37e01c6ec0c98599778c2e688350d624fa4770fbd6144551bd5e032f1199171c \ + --hash=sha256:401b1941ce37e78b8fd119b419b617277c65ae9417742a63282257434fd68ea2 \ + --hash=sha256:4536f2695fe5c1ffb528d84a35a7d3967e5558d2af58b4775e7ab1449d65767b \ + --hash=sha256:4c908a7036f0f3677f8afa577bcd973e3e20ddd2f7c42a33208d18bee95cdb6f \ + --hash=sha256:51ab1ff33c19e336c02dee1e9fd1abd974a4ca3d8f7eef2a104d0816a241ce97 \ + --hash=sha256:524133c1be38445c5c0575eacea42dbd44374b310b1ffc4b60ff01d881fabb96 \ + --hash=sha256:57bb7e26928573ee7c6504f54c05860d867fd35e675769f3ce01b52af38d48e2 \ + --hash=sha256:60f6665579e909b618282f3c14fa0b80570fbf1ee0e67678b9a9d43aa5d67a37 \ + --hash=sha256:62224a9bb85b4b66d1b46d45cbe43d71cbf8f527d332b177e3b96191ffbc1e64 \ + --hash=sha256:6770d7da00f358183d8fd5c4615436189e4f683bdb6affb02cad3d221d7bb757 \ + --hash=sha256:6801aeddb6acb2c42eafa45bc1cb98ba236871ae6f33f31e984670b749a8e58e \ + --hash=sha256:70d6b3ceaa9cc5a6ac52884f3b3d9544e8e231e95b23f138bdb78e6d4dc0eae3 \ + --hash=sha256:78813b49d749e1bb4db1c57f2d4d7e6db22c253cb0a86ad819f5dc197710d4b2 \ + --hash=sha256:841b2186adce48903c0fef235421ae21549020eca942c1da773ac380b056ab3c \ + --hash=sha256:84fc186980231a287b28560d3123bd255d3c6b6659828c642b4cf961e2b923d0 \ + --hash=sha256:885bde7d26e5b40e15c47bd5def48b38cbd50830a65f98122a8fb90962af7cd1 \ + --hash=sha256:8b4309a2775e4feee7356e63b163969a215d663399cce1b3d3b65e7ec2d9680e \ + --hash=sha256:8d77f92438daeaddc05682f0f3dac90c5b9829bcac75b57e8ce09cb67786073c \ + --hash=sha256:902425f5afe28572d65d2bf9c33edd5265c612ff82c69e6f83ea13eafc0dcbea \ + --hash=sha256:9bcc1e77fbd1609198966ded6b2a9897bd6c6bcbd2287a2fc7d75f1a254179c5 \ + --hash=sha256:a408c3c51358c89b29cfa5317cf11518b7ce5de1717abb55c5ae2d2921027de6 \ + --hash=sha256:a9bf8adc9e1f3012edc8f09b08336272aec0c55bc677422273e21280db748f7c \ + --hash=sha256:b818db35879d2edf7f46c7e729c700a0bce03b61b9412f5a7118406687cb151d \ + --hash=sha256:b8974b2a266b54c96709bd5e239979cddfd2dbceed331aa567ea1d7c4a2202db \ + --hash=sha256:be392ec3529e2f57faa28709d60723a763904f71a2b63aabe14fee6648fe3b14 \ + --hash=sha256:d3972b13148c1d1fbc092b27678a33b3080d1ac0ca305742b0119b75f9e87e38 \ + --hash=sha256:d40dcf533ca481355aa7b682e9e079f766f35715defa4929aeb5597f9604272e \ + --hash=sha256:e93df708c69a193fc7987192f94df250f83f3851fda49413f02ba5dded639482 \ + --hash=sha256:efd7e6660674e234e29937bc1481dceb7e0336bfae75b856b4fb272b5093c5d4 \ + --hash=sha256:f9b3a78f69dcbd803cf2fb3f972779875b244c1115481dfbdd567b2c22b31f6b \ + --hash=sha256:fa39475eaccb98f9199eccfda4298abaf35ae0caec676ffc25b3a5e224044464 \ + --hash=sha256:fbce6dae41b692a5973d0f2158f782b9ad05babc2c2019a970a1094a23909b1b + # via weasyprint +identify==2.6.13 \ + --hash=sha256:60381139b3ae39447482ecc406944190f690d4a2997f2584062089848361b33b \ + --hash=sha256:da8d6c828e773620e13bfa86ea601c5a5310ba4bcd65edf378198b56a1f9fb32 + # via pre-commit +nodeenv==1.9.1 \ + --hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \ + --hash=sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9 + # via pre-commit +numpy==2.2.6 \ + --hash=sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff \ + --hash=sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47 \ + --hash=sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84 \ + --hash=sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d \ + --hash=sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6 \ + --hash=sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f \ + --hash=sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b \ + --hash=sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49 \ + --hash=sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163 \ + --hash=sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571 \ + --hash=sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42 \ + --hash=sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff \ + --hash=sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491 \ + --hash=sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4 \ + --hash=sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566 \ + --hash=sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf \ + --hash=sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40 \ + --hash=sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd \ + --hash=sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06 \ + --hash=sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282 \ + --hash=sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680 \ + --hash=sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db \ + --hash=sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3 \ + --hash=sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90 \ + --hash=sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1 \ + --hash=sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289 \ + --hash=sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab \ + --hash=sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c \ + --hash=sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d \ + --hash=sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb \ + --hash=sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d \ + --hash=sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a \ + --hash=sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf \ + --hash=sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1 \ + --hash=sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2 \ + --hash=sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a \ + --hash=sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543 \ + --hash=sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00 \ + --hash=sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c \ + --hash=sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f \ + --hash=sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd \ + --hash=sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868 \ + --hash=sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303 \ + --hash=sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83 \ + --hash=sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3 \ + --hash=sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d \ + --hash=sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87 \ + --hash=sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa \ + --hash=sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f \ + --hash=sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae \ + --hash=sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda \ + --hash=sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915 \ + --hash=sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249 \ + --hash=sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de \ + --hash=sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8 + # via opencv-python-headless +opencv-python-headless==4.12.0.88 \ + --hash=sha256:1e58d664809b3350c1123484dd441e1667cd7bed3086db1b9ea1b6f6cb20b50e \ + --hash=sha256:236c8df54a90f4d02076e6f9c1cc763d794542e886c576a6fee46ec8ff75a7a9 \ + --hash=sha256:365bb2e486b50feffc2d07a405b953a8f3e8eaa63865bc650034e5c71e7a5154 \ + --hash=sha256:86b413bdd6c6bf497832e346cd5371995de148e579b9774f8eba686dee3f5528 \ + --hash=sha256:aeb4b13ecb8b4a0beb2668ea07928160ea7c2cd2d9b5ef571bbee6bafe9cc8d0 \ + --hash=sha256:cfdc017ddf2e59b6c2f53bc12d74b6b0be7ded4ec59083ea70763921af2b6c09 \ + --hash=sha256:fde2cf5c51e4def5f2132d78e0c08f9c14783cd67356922182c6845b9af87dbd + # via -r .github/scripts/requirements_dev.in +pdf2image==1.17.0 \ + --hash=sha256:eaa959bc116b420dd7ec415fcae49b98100dda3dd18cd2fdfa86d09f112f6d57 \ + --hash=sha256:ecdd58d7afb810dffe21ef2b1bbc057ef434dabbac6c33778a38a3f7744a27e2 + # via -r .github/scripts/requirements_dev.in +pillow==11.3.0 \ + --hash=sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2 \ + --hash=sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214 \ + --hash=sha256:040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e \ + --hash=sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59 \ + --hash=sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50 \ + --hash=sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632 \ + --hash=sha256:092c80c76635f5ecb10f3f83d76716165c96f5229addbd1ec2bdbbda7d496e06 \ + --hash=sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a \ + --hash=sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51 \ + --hash=sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced \ + --hash=sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f \ + --hash=sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12 \ + --hash=sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8 \ + --hash=sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6 \ + --hash=sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580 \ + --hash=sha256:19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f \ + --hash=sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac \ + --hash=sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860 \ + --hash=sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd \ + --hash=sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722 \ + --hash=sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8 \ + --hash=sha256:23cff760a9049c502721bdb743a7cb3e03365fafcdfc2ef9784610714166e5a4 \ + --hash=sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673 \ + --hash=sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788 \ + --hash=sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542 \ + --hash=sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e \ + --hash=sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd \ + --hash=sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8 \ + --hash=sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523 \ + --hash=sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967 \ + --hash=sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809 \ + --hash=sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477 \ + --hash=sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027 \ + --hash=sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae \ + --hash=sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b \ + --hash=sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c \ + --hash=sha256:48d254f8a4c776de343051023eb61ffe818299eeac478da55227d96e241de53f \ + --hash=sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e \ + --hash=sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b \ + --hash=sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7 \ + --hash=sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27 \ + --hash=sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361 \ + --hash=sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae \ + --hash=sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d \ + --hash=sha256:6359a3bc43f57d5b375d1ad54a0074318a0844d11b76abccf478c37c986d3cfc \ + --hash=sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58 \ + --hash=sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad \ + --hash=sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6 \ + --hash=sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024 \ + --hash=sha256:6a418691000f2a418c9135a7cf0d797c1bb7d9a485e61fe8e7722845b95ef978 \ + --hash=sha256:6abdbfd3aea42be05702a8dd98832329c167ee84400a1d1f61ab11437f1717eb \ + --hash=sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d \ + --hash=sha256:7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0 \ + --hash=sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9 \ + --hash=sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f \ + --hash=sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874 \ + --hash=sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa \ + --hash=sha256:7aee118e30a4cf54fdd873bd3a29de51e29105ab11f9aad8c32123f58c8f8081 \ + --hash=sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149 \ + --hash=sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6 \ + --hash=sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d \ + --hash=sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd \ + --hash=sha256:819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f \ + --hash=sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c \ + --hash=sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31 \ + --hash=sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e \ + --hash=sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db \ + --hash=sha256:89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6 \ + --hash=sha256:8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f \ + --hash=sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494 \ + --hash=sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69 \ + --hash=sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94 \ + --hash=sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77 \ + --hash=sha256:97afb3a00b65cc0804d1c7abddbf090a81eaac02768af58cbdcaaa0a931e0b6d \ + --hash=sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7 \ + --hash=sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a \ + --hash=sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438 \ + --hash=sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288 \ + --hash=sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b \ + --hash=sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635 \ + --hash=sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3 \ + --hash=sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d \ + --hash=sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe \ + --hash=sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0 \ + --hash=sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe \ + --hash=sha256:be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a \ + --hash=sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805 \ + --hash=sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8 \ + --hash=sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36 \ + --hash=sha256:cadc9e0ea0a2431124cde7e1697106471fc4c1da01530e679b2391c37d3fbb3a \ + --hash=sha256:cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b \ + --hash=sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e \ + --hash=sha256:d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25 \ + --hash=sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12 \ + --hash=sha256:e5c5858ad8ec655450a7c7df532e9842cf8df7cc349df7225c60d5d348c8aada \ + --hash=sha256:e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c \ + --hash=sha256:ea944117a7974ae78059fcc1800e5d3295172bb97035c0c1d9345fca1419da71 \ + --hash=sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d \ + --hash=sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c \ + --hash=sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6 \ + --hash=sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1 \ + --hash=sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50 \ + --hash=sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653 \ + --hash=sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c \ + --hash=sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4 \ + --hash=sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3 + # via + # -r .github/scripts/requirements_dev.in + # pdf2image + # weasyprint +platformdirs==4.3.8 \ + --hash=sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc \ + --hash=sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4 + # via virtualenv +pre-commit==4.3.0 \ + --hash=sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8 \ + --hash=sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16 + # via -r .github/scripts/requirements_dev.in +pycparser==2.22 \ + --hash=sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6 \ + --hash=sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc + # via cffi +pydyf==0.11.0 \ + --hash=sha256:0aaf9e2ebbe786ec7a78ec3fbffa4cdcecde53fd6f563221d53c6bc1328848a3 \ + --hash=sha256:394dddf619cca9d0c55715e3c55ea121a9bf9cbc780cdc1201a2427917b86b64 + # via weasyprint +pyphen==0.17.2 \ + --hash=sha256:3a07fb017cb2341e1d9ff31b8634efb1ae4dc4b130468c7c39dd3d32e7c3affd \ + --hash=sha256:f60647a9c9b30ec6c59910097af82bc5dd2d36576b918e44148d8b07ef3b4aa3 + # via weasyprint +pyyaml==6.0.2 \ + --hash=sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff \ + --hash=sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48 \ + --hash=sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086 \ + --hash=sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e \ + --hash=sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133 \ + --hash=sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5 \ + --hash=sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484 \ + --hash=sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee \ + --hash=sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5 \ + --hash=sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68 \ + --hash=sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a \ + --hash=sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf \ + --hash=sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99 \ + --hash=sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8 \ + --hash=sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85 \ + --hash=sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19 \ + --hash=sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc \ + --hash=sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a \ + --hash=sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1 \ + --hash=sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317 \ + --hash=sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c \ + --hash=sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631 \ + --hash=sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d \ + --hash=sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652 \ + --hash=sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5 \ + --hash=sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e \ + --hash=sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b \ + --hash=sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8 \ + --hash=sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476 \ + --hash=sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706 \ + --hash=sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563 \ + --hash=sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237 \ + --hash=sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b \ + --hash=sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083 \ + --hash=sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180 \ + --hash=sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425 \ + --hash=sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e \ + --hash=sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f \ + --hash=sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725 \ + --hash=sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183 \ + --hash=sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab \ + --hash=sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774 \ + --hash=sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725 \ + --hash=sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e \ + --hash=sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5 \ + --hash=sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d \ + --hash=sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290 \ + --hash=sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44 \ + --hash=sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed \ + --hash=sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4 \ + --hash=sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba \ + --hash=sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12 \ + --hash=sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4 + # via pre-commit +tinycss2==1.4.0 \ + --hash=sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7 \ + --hash=sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289 + # via + # cssselect2 + # weasyprint +tinyhtml5==2.0.0 \ + --hash=sha256:086f998833da24c300c414d9fe81d9b368fd04cb9d2596a008421cbc705fcfcc \ + --hash=sha256:13683277c5b176d070f82d099d977194b7a1e26815b016114f581a74bbfbf47e + # via weasyprint +unoserver==3.3.2 \ + --hash=sha256:1eeb7467cf6b56b8eff3b576e2d1b2b2ff4e0eb2052e995ac80a1456de300639 \ + --hash=sha256:87e144f903ee21951b2e06a97549450c13ed7eca5bcebad942d3352d4e882616 + # via -r .github/scripts/requirements_dev.in +virtualenv==20.33.1 \ + --hash=sha256:07c19bc66c11acab6a5958b815cbcee30891cd1c2ccf53785a28651a0d8d8a67 \ + --hash=sha256:1b44478d9e261b3fb8baa5e74a0ca3bc0e05f21aa36167bf9cbf850e542765b8 + # via pre-commit +weasyprint==66.0 \ + --hash=sha256:82b0783b726fcd318e2c977dcdddca76515b30044bc7a830cc4fbe717582a6d0 \ + --hash=sha256:da71dc87dc129ac9cffdc65e5477e90365ab9dbae45c744014ec1d06303dde40 + # via -r .github/scripts/requirements_dev.in +webencodings==0.5.1 \ + --hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \ + --hash=sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923 + # via + # cssselect2 + # tinycss2 + # tinyhtml5 +zopfli==0.2.3.post1 \ + --hash=sha256:0aa5f90d6298bda02a95bc8dc8c3c19004d5a4e44bda00b67ca7431d857b4b54 \ + --hash=sha256:0cc20b02a9531559945324c38302fd4ba763311632d0ec8a1a0aa9c10ea363e6 \ + --hash=sha256:1d8cc06605519e82b16df090e17cb3990d1158861b2872c3117f1168777b81e4 \ + --hash=sha256:1f990634fd5c5c8ced8edddd8bd45fab565123b4194d6841e01811292650acae \ + --hash=sha256:2345e713260a350bea0b01a816a469ea356bc2d63d009a0d777691ecbbcf7493 \ + --hash=sha256:2768c877f76c8a0e7519b1c86c93757f3c01492ddde55751e9988afb7eff64e1 \ + --hash=sha256:29ea74e72ffa6e291b8c6f2504ce6c146b4fe990c724c1450eb8e4c27fd31431 \ + --hash=sha256:34a99592f3d9eb6f737616b5bd74b48a589fdb3cb59a01a50d636ea81d6af272 \ + --hash=sha256:3654bfc927bc478b1c3f3ff5056ed7b20a1a37fa108ca503256d0a699c03bbb1 \ + --hash=sha256:3657e416ffb8f31d9d3424af12122bb251befae109f2e271d87d825c92fc5b7b \ + --hash=sha256:37d011e92f7b9622742c905fdbed9920a1d0361df84142807ea2a528419dea7f \ + --hash=sha256:3827170de28faf144992d3d4dcf8f3998fe3c8a6a6f4a08f1d42c2ec6119d2bb \ + --hash=sha256:39e576f93576c5c223b41d9c780bbb91fd6db4babf3223d2a4fe7bf568e2b5a8 \ + --hash=sha256:3a89277ed5f8c0fb2d0b46d669aa0633123aa7381f1f6118c12f15e0fb48f8ca \ + --hash=sha256:3c163911f8bad94b3e1db0a572e7c28ba681a0c91d0002ea1e4fa9264c21ef17 \ + --hash=sha256:3f0197b6aa6eb3086ae9e66d6dd86c4d502b6c68b0ec490496348ae8c05ecaef \ + --hash=sha256:48dba9251060289101343110ab47c0756f66f809bb4d1ddbb6d5c7e7752115c5 \ + --hash=sha256:4915a41375bdee4db749ecd07d985a0486eb688a6619f713b7bf6fbfd145e960 \ + --hash=sha256:4c1226a7e2c7105ac31503a9bb97454743f55d88164d6d46bc138051b77f609b \ + --hash=sha256:4e50ffac74842c1c1018b9b73875a0d0a877c066ab06bf7cccbaa84af97e754f \ + --hash=sha256:518f1f4ed35dd69ce06b552f84e6d081f07c552b4c661c5312d950a0b764a58a \ + --hash=sha256:5aad740b4d4fcbaaae4887823925166ffd062db3b248b3f432198fc287381d1a \ + --hash=sha256:5f272186e03ad55e7af09ab78055535c201b1a0bcc2944edb1768298d9c483a4 \ + --hash=sha256:5fcfc0dc2761e4fcc15ad5d273b4d58c2e8e059d3214a7390d4d3c8e2aee644e \ + --hash=sha256:60db20f06c3d4c5934b16cfa62a2cc5c3f0686bffe0071ed7804d3c31ab1a04e \ + --hash=sha256:615a8ac9dda265e9cc38b2a76c3142e4a9f30fea4a79c85f670850783bc6feb4 \ + --hash=sha256:6482db9876c68faac2d20a96b566ffbf65ddaadd97b222e4e73641f4f8722fc4 \ + --hash=sha256:6617fb10f9e4393b331941861d73afb119cd847e88e4974bdbe8068ceef3f73f \ + --hash=sha256:676919fba7311125244eb0c4393679ac5fe856e5864a15d122bd815205369fa0 \ + --hash=sha256:6c2d2bc8129707e34c51f9352c4636ca313b52350bbb7e04637c46c1818a2a70 \ + --hash=sha256:71390dbd3fbf6ebea9a5d85ffed8c26ee1453ee09248e9b88486e30e0397b775 \ + --hash=sha256:716cdbfc57bfd3d3e31a58e6246e8190e6849b7dbb7c4ce39ef8bbf0edb8f6d5 \ + --hash=sha256:75a26a2307b10745a83b660c404416e984ee6fca515ec7f0765f69af3ce08072 \ + --hash=sha256:7be5cc6732eb7b4df17305d8a7b293223f934a31783a874a01164703bc1be6cd \ + --hash=sha256:7cce242b5df12b2b172489daf19c32e5577dd2fac659eb4b17f6a6efb446fd5c \ + --hash=sha256:81c341d9bb87a6dbbb0d45d6e272aca80c7c97b4b210f9b6e233bf8b87242f29 \ + --hash=sha256:89899641d4de97dbad8e0cde690040d078b6aea04066dacaab98e0b5a23573f2 \ + --hash=sha256:8d5ab297d660b75c159190ce6d73035502310e40fd35170aed7d1a1aea7ddd65 \ + --hash=sha256:8fbe5bcf10d01aab3513550f284c09fef32f342b36f56bfae2120a9c4d12c130 \ + --hash=sha256:91a2327a4d7e77471fa4fbb26991c6de4a738c6fc6a33e09bb25f56a870a4b7b \ + --hash=sha256:95a260cafd56b8fffa679918937401c80bb38e1681c448b988022e4c3610965d \ + --hash=sha256:96484dc0f48be1c5d7ae9f38ed1ce41e3675fd506b27c11a6607f14b49101e99 \ + --hash=sha256:9a6aec38a989bad7ddd1ef53f1265699e49e294d08231b5313d61293f3cd6237 \ + --hash=sha256:9ba214f4f45bec195ee8559651154d3ac2932470b9d91c5715fc29c013349f8c \ + --hash=sha256:9f4a7ec2770e6af05f5a02733fd3900f30a9cd58e5d6d3727e14c5bcd6e7d587 \ + --hash=sha256:a1cf720896d2ce998bc8e051d4b4ce0d8bec007aab6243102e8e1d22a0b2fb3f \ + --hash=sha256:a241a68581d34d67b40c425cce3d1fd211c092f99d9250947824ccba9f491949 \ + --hash=sha256:a53b18797cdef27e019db595d66c4b077325afe2fd62145953275f53d84ce40c \ + --hash=sha256:a82fc2dbebe6eb908b9c665e71496f8525c1bc4d2e3a7a7722ef2b128b6227c8 \ + --hash=sha256:a86eb88e06bd87e1fff31dac878965c26b0c26db59ddcf78bb0379a954b120de \ + --hash=sha256:aa588b21044f8a74e423d8c8a4c7fc9988501878aacced793467010039c50734 \ + --hash=sha256:b05296e8bc88c92e2b21e0a9bae4740c1551ee613c1d93a51fd28a7a0b2b6fbb \ + --hash=sha256:b0ec13f352ea5ae0fc91f98a48540512eed0767d0ec4f7f3cb92d92797983d18 \ + --hash=sha256:b3df42f52502438ee973042cc551877d24619fa1cd38ef7b7e9ac74200daca8b \ + --hash=sha256:b78008a69300d929ca2efeffec951b64a312e9a811e265ea4a907ab546d79fa6 \ + --hash=sha256:b9026a21b6d41eb0e2e63f5bc1242c3fcc43ecb770963cda99a4307863dac12e \ + --hash=sha256:bbe429fc50686bb2a2608a30843e36fbaa123462a5284f136c7d9e0145220bfd \ + --hash=sha256:bfa1eb759e07d8b7aa7a310a2bc535e127ee70addf90dc8d4b946b593c3e51a8 \ + --hash=sha256:c1e0ed5d84ffa2d677cc9582fc01e61dab2e7ef8b8996e055f0a76167b1b94df \ + --hash=sha256:c4278d1873ce6e803e5d4f8d702fd3026bd67fca744aa98881324d1157ddf748 \ + --hash=sha256:cac2b37ab21c2b36a10b685b1893ebd6b0f83ae26004838ac817680881576567 \ + --hash=sha256:cbe6df25807227519debd1a57ab236f5f6bad441500e85b13903e51f93a43214 \ + --hash=sha256:cd2c002f160502608dcc822ed2441a0f4509c52e86fcfd1a09e937278ed1ca14 \ + --hash=sha256:e0137dd64a493ba6a4be37405cfd6febe650a98cc1e9dca8f6b8c63b1db11b41 \ + --hash=sha256:e63d558847166543c2c9789e6f985400a520b7eacc4b99181668b2c3aeadd352 \ + --hash=sha256:eb45a34f23da4f8bc712b6376ca5396914b0b7c09adbb001dad964eb7f3132f8 \ + --hash=sha256:ecb7572df5372abce8073df078207d9d1749f20b8b136089916a4a0868d56051 \ + --hash=sha256:f12000a6accdd4bf0a3fa6eaa1b1c7a7bc80af0a2edf3f89d770d3dcce1d0e22 \ + --hash=sha256:f7d69c1a7168ad0e9cb864e8663acb232986a0c9c9cb9801f56bf6214f53a54d \ + --hash=sha256:f815fcc2b2a457977724bad97fb4854022980f51ce7b136925e336b530545ae1 \ + --hash=sha256:fc39f5c27f962ec8660d8d20c24762431131b5d8c672b44b0a54cf2b5bcde9b9 + # via fonttools + +# The following packages are considered to be unsafe in a requirements file: +pip==25.2 \ + --hash=sha256:578283f006390f85bb6282dffb876454593d637f5d1be494b5202ce4877e71f2 \ + --hash=sha256:6d67a2b4e7f14d8b31b8b52648866fa717f45a1eb70e83002f4331d07e953717 + # via -r .github/scripts/requirements_dev.in +setuptools==80.9.0 \ + --hash=sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 \ + --hash=sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c + # via -r .github/scripts/requirements_dev.in diff --git a/.github/scripts/requirements_pre_commit.txt b/.github/scripts/requirements_pre_commit.txt index 4e2d2c2b6..60b304142 100644 --- a/.github/scripts/requirements_pre_commit.txt +++ b/.github/scripts/requirements_pre_commit.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.10 +# This file is autogenerated by pip-compile with Python 3.12 # by the following command: # # pip-compile --generate-hashes --output-file='.github\scripts\requirements_pre_commit.txt' --strip-extras '.github\scripts\requirements_pre_commit.in' @@ -8,86 +8,106 @@ cfgv==3.4.0 \ --hash=sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9 \ --hash=sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560 # via pre-commit -distlib==0.3.9 \ - --hash=sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87 \ - --hash=sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403 +distlib==0.4.0 \ + --hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \ + --hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d # via virtualenv -filelock==3.18.0 \ - --hash=sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2 \ - --hash=sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de +filelock==3.19.1 \ + --hash=sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58 \ + --hash=sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d # via virtualenv -identify==2.6.12 \ - --hash=sha256:ad9672d5a72e0d2ff7c5c8809b62dfa60458626352fb0eb7b55e69bdc45334a2 \ - --hash=sha256:d8de45749f1efb108badef65ee8386f0f7bb19a7f26185f74de6367bffbaf0e6 +identify==2.6.15 \ + --hash=sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757 \ + --hash=sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf # via pre-commit nodeenv==1.9.1 \ --hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \ --hash=sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9 # via pre-commit -platformdirs==4.3.8 \ - --hash=sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc \ - --hash=sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4 +platformdirs==4.4.0 \ + --hash=sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85 \ + --hash=sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf # via virtualenv -pre-commit==4.2.0 \ - --hash=sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146 \ - --hash=sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd - # via -r .github\scripts\requirements_pre_commit.in -pyyaml==6.0.2 \ - --hash=sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff \ - --hash=sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48 \ - --hash=sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086 \ - --hash=sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e \ - --hash=sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133 \ - --hash=sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5 \ - --hash=sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484 \ - --hash=sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee \ - --hash=sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5 \ - --hash=sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68 \ - --hash=sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a \ - --hash=sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf \ - --hash=sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99 \ - --hash=sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8 \ - --hash=sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85 \ - --hash=sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19 \ - --hash=sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc \ - --hash=sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a \ - --hash=sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1 \ - --hash=sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317 \ - --hash=sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c \ - --hash=sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631 \ - --hash=sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d \ - --hash=sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652 \ - --hash=sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5 \ - --hash=sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e \ - --hash=sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b \ - --hash=sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8 \ - --hash=sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476 \ - --hash=sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706 \ - --hash=sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563 \ - --hash=sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237 \ - --hash=sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b \ - --hash=sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083 \ - --hash=sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180 \ - --hash=sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425 \ - --hash=sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e \ - --hash=sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f \ - --hash=sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725 \ - --hash=sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183 \ - --hash=sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab \ - --hash=sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774 \ - --hash=sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725 \ - --hash=sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e \ - --hash=sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5 \ - --hash=sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d \ - --hash=sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290 \ - --hash=sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44 \ - --hash=sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed \ - --hash=sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4 \ - --hash=sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba \ - --hash=sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12 \ - --hash=sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4 +pre-commit==4.3.0 \ + --hash=sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8 \ + --hash=sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16 + # via -r .github/scripts/requirements_pre_commit.in +pyyaml==6.0.3 \ + --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \ + --hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \ + --hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \ + --hash=sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956 \ + --hash=sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6 \ + --hash=sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c \ + --hash=sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65 \ + --hash=sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a \ + --hash=sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0 \ + --hash=sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b \ + --hash=sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 \ + --hash=sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6 \ + --hash=sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7 \ + --hash=sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e \ + --hash=sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007 \ + --hash=sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310 \ + --hash=sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4 \ + --hash=sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9 \ + --hash=sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295 \ + --hash=sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea \ + --hash=sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0 \ + --hash=sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e \ + --hash=sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac \ + --hash=sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9 \ + --hash=sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7 \ + --hash=sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35 \ + --hash=sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb \ + --hash=sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b \ + --hash=sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69 \ + --hash=sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5 \ + --hash=sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b \ + --hash=sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c \ + --hash=sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369 \ + --hash=sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd \ + --hash=sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824 \ + --hash=sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198 \ + --hash=sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065 \ + --hash=sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c \ + --hash=sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c \ + --hash=sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764 \ + --hash=sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196 \ + --hash=sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b \ + --hash=sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00 \ + --hash=sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac \ + --hash=sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8 \ + --hash=sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e \ + --hash=sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 \ + --hash=sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3 \ + --hash=sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5 \ + --hash=sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4 \ + --hash=sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b \ + --hash=sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf \ + --hash=sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5 \ + --hash=sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702 \ + --hash=sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8 \ + --hash=sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788 \ + --hash=sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da \ + --hash=sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d \ + --hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc \ + --hash=sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c \ + --hash=sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba \ + --hash=sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f \ + --hash=sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917 \ + --hash=sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5 \ + --hash=sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26 \ + --hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \ + --hash=sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b \ + --hash=sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be \ + --hash=sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c \ + --hash=sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3 \ + --hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \ + --hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \ + --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0 # via pre-commit -virtualenv==20.31.2 \ - --hash=sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11 \ - --hash=sha256:e10c0a9d02835e592521be48b332b6caee6887f332c111aa79a09b9e79efc2af +virtualenv==20.34.0 \ + --hash=sha256:341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026 \ + --hash=sha256:44815b2c9dee7ed86e387b842a84f20b93f7f417f95886ca1996a72a4138eb1a # via pre-commit diff --git a/.github/scripts/requirements_sync_readme.txt b/.github/scripts/requirements_sync_readme.txt index e72486ab2..eb0cd9bf7 100644 --- a/.github/scripts/requirements_sync_readme.txt +++ b/.github/scripts/requirements_sync_readme.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.10 +# This file is autogenerated by pip-compile with Python 3.12 # by the following command: # # pip-compile --generate-hashes --output-file='.github\scripts\requirements_sync_readme.txt' --strip-extras '.github\scripts\requirements_sync_readme.in' @@ -7,4 +7,4 @@ tomlkit==0.13.3 \ --hash=sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1 \ --hash=sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0 - # via -r .github\scripts\requirements_sync_readme.in + # via -r .github/scripts/requirements_sync_readme.in diff --git a/.github/workflows/PR-Demo-Comment-with-react.yml b/.github/workflows/PR-Demo-Comment-with-react.yml index 4acd0cbe6..82f1e0140 100644 --- a/.github/workflows/PR-Demo-Comment-with-react.yml +++ b/.github/workflows/PR-Demo-Comment-with-react.yml @@ -34,20 +34,18 @@ jobs: ) outputs: pr_number: ${{ steps.get-pr.outputs.pr_number }} - pr_repository: ${{ steps.get-pr-info.outputs.repository }} - pr_ref: ${{ steps.get-pr-info.outputs.ref }} comment_id: ${{ github.event.comment.id }} disable_security: ${{ steps.check-security-flag.outputs.disable_security }} enable_pro: ${{ steps.check-pro-flag.outputs.enable_pro }} enable_enterprise: ${{ steps.check-pro-flag.outputs.enable_enterprise }} steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - name: Checkout PR - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup GitHub App Bot if: github.actor != 'dependabot[bot]' @@ -60,36 +58,13 @@ jobs: - name: Get PR data id: get-pr - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: script: | const prNumber = context.payload.issue.number; console.log(`PR Number: ${prNumber}`); core.setOutput('pr_number', prNumber); - - name: Get PR repository and ref - id: get-pr-info - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 - with: - script: | - const { owner, repo } = context.repo; - const prNumber = context.payload.issue.number; - - const { data: pr } = await github.rest.pulls.get({ - owner, - repo, - pull_number: prNumber, - }); - - // For forks, use the full repository name, for internal PRs use the current repo - const repository = pr.head.repo.fork ? pr.head.repo.full_name : `${owner}/${repo}`; - - console.log(`PR Repository: ${repository}`); - console.log(`PR Branch: ${pr.head.ref}`); - - core.setOutput('repository', repository); - core.setOutput('ref', pr.head.ref); - - name: Check for security/login flag id: check-security-flag env: @@ -124,7 +99,7 @@ jobs: - name: Add 'in_progress' reaction to comment id: add-eyes-reaction - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: github-token: ${{ steps.setup-bot.outputs.token }} script: | @@ -153,12 +128,12 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - name: Checkout PR - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup GitHub App Bot if: github.actor != 'dependabot[bot]' @@ -170,14 +145,13 @@ jobs: private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - name: Checkout PR - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: - repository: ${{ needs.check-comment.outputs.pr_repository }} - ref: ${{ needs.check-comment.outputs.pr_ref }} + ref: refs/pull/${{ needs.check-comment.outputs.pr_number }}/merge token: ${{ steps.setup-bot.outputs.token }} - name: Set up JDK - uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 + uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0 with: java-version: "17" distribution: "temurin" @@ -197,7 +171,7 @@ jobs: uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 - name: Login to Docker Hub - uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 + uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_API }} @@ -297,7 +271,7 @@ jobs: - name: Add success reaction to comment if: success() - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: github-token: ${{ steps.setup-bot.outputs.token }} script: | @@ -332,7 +306,7 @@ jobs: - name: Add failure reaction to comment if: failure() - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: github-token: ${{ steps.setup-bot.outputs.token }} script: | @@ -352,7 +326,7 @@ jobs: - name: Post deployment URL to PR if: success() - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: github-token: ${{ steps.setup-bot.outputs.token }} script: | diff --git a/.github/workflows/PR-Demo-cleanup.yml b/.github/workflows/PR-Demo-cleanup.yml index 67625c0a5..47f1e8ed9 100644 --- a/.github/workflows/PR-Demo-cleanup.yml +++ b/.github/workflows/PR-Demo-cleanup.yml @@ -21,12 +21,12 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - name: Checkout PR - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup GitHub App Bot if: github.actor != 'dependabot[bot]' @@ -39,7 +39,7 @@ jobs: - name: Remove 'pr-deployed' label if present id: remove-label-comment - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: github-token: ${{ steps.setup-bot.outputs.token }} script: | diff --git a/.github/workflows/ai_pr_title_review.yml b/.github/workflows/ai_pr_title_review.yml index 59a69ae5f..77668d69a 100644 --- a/.github/workflows/ai_pr_title_review.yml +++ b/.github/workflows/ai_pr_title_review.yml @@ -19,11 +19,11 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 0 @@ -158,7 +158,7 @@ jobs: - name: Post comment on PR if needed if: steps.actor.outputs.is_repo_dev == 'true' - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 continue-on-error: true with: github-token: ${{ steps.setup-bot.outputs.token }} diff --git a/.github/workflows/auto-labelerV2.yml b/.github/workflows/auto-labelerV2.yml index fae92940f..d66ea570a 100644 --- a/.github/workflows/auto-labelerV2.yml +++ b/.github/workflows/auto-labelerV2.yml @@ -13,11 +13,11 @@ jobs: pull-requests: write steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup GitHub App Bot id: setup-bot diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index be04dbd64..03f684c4f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -35,7 +35,6 @@ jobs: uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Check for file changes @@ -123,7 +122,8 @@ jobs: with: java-version: "17" distribution: "temurin" - - uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0 + - name: Setup Gradle + uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0 - name: Generate OpenAPI documentation run: ./gradlew :stirling-pdf:generateOpenApiDocs env: @@ -183,8 +183,10 @@ jobs: with: java-version: "17" distribution: "temurin" + - name: check the licenses for compatibility run: ./gradlew clean checkLicense + - name: FAILED - check the licenses for compatibility if: failure() uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 diff --git a/.github/workflows/check_properties.yml b/.github/workflows/check_properties.yml index 8633d2d62..73232eee9 100644 --- a/.github/workflows/check_properties.yml +++ b/.github/workflows/check_properties.yml @@ -30,12 +30,12 @@ jobs: pull-requests: write # Allow writing to pull requests steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - name: Checkout main branch first - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup GitHub App Bot id: setup-bot @@ -46,7 +46,7 @@ jobs: - name: Get PR data id: get-pr-data - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: github-token: ${{ steps.setup-bot.outputs.token }} script: | @@ -90,7 +90,7 @@ jobs: - name: Determine reference file test id: determine-file - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: github-token: ${{ steps.setup-bot.outputs.token }} script: | @@ -234,7 +234,7 @@ jobs: - name: Post comment on PR if: env.SCRIPT_OUTPUT != '' - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: github-token: ${{ steps.setup-bot.outputs.token }} script: | diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 8d938011d..b2a66c2b6 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -17,11 +17,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - name: "Checkout Repository" - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: "Dependency Review" - uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d9 # v4.7.1 + uses: actions/dependency-review-action@56339e523c0409420f6c2c9a2f4292bbb3c07dd3 # v4.8.0 diff --git a/.github/workflows/licenses-update.yml b/.github/workflows/licenses-update.yml index 1f920e2da..a3dadc272 100644 --- a/.github/workflows/licenses-update.yml +++ b/.github/workflows/licenses-update.yml @@ -31,12 +31,12 @@ jobs: repository-projects: write # Required for enabling automerge steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - name: Check out code - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 0 @@ -48,16 +48,19 @@ jobs: private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - name: Set up JDK 17 - uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 + uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0 with: java-version: "17" distribution: "temurin" - name: Setup Gradle - uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2 + uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0 - name: Check licenses for compatibility run: ./gradlew clean checkLicense + env: + DISABLE_ADDITIONAL_FEATURES: false + STIRLING_PDF_DESKTOP_UI: true - name: Upload artifact on failure if: failure() diff --git a/.github/workflows/manage-label.yml b/.github/workflows/manage-label.yml index 3f25fbaf1..d480249f2 100644 --- a/.github/workflows/manage-label.yml +++ b/.github/workflows/manage-label.yml @@ -15,12 +15,12 @@ jobs: issues: write steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - name: Check out the repository - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Run Labeler uses: crazy-max/ghaction-github-labeler@24d110aa46a59976b8a7f35518cb7f14f434c916 # v5.3.0 diff --git a/.github/workflows/multiOSReleases.yml b/.github/workflows/multiOSReleases.yml index e043fd094..1e4a18f91 100644 --- a/.github/workflows/multiOSReleases.yml +++ b/.github/workflows/multiOSReleases.yml @@ -21,14 +21,14 @@ jobs: versionMac: ${{ steps.versionNumberMac.outputs.versionNumberMac }} steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Set up JDK - uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 + uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0 with: distribution: 'temurin' java-version: '21' @@ -60,19 +60,19 @@ jobs: file_suffix: "" steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Set up JDK 21 - uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 + uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0 with: java-version: "21" distribution: "temurin" - - uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2 + - uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0 with: gradle-version: 8.14 @@ -110,7 +110,7 @@ jobs: file_suffix: "" steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit @@ -148,19 +148,19 @@ jobs: contents: write steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - name: Set up JDK 21 - uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 + uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0 with: java-version: "21" distribution: "temurin" - - uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2 + - uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0 with: gradle-version: 8.14 @@ -238,7 +238,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit @@ -252,7 +252,7 @@ jobs: - name: Install Cosign if: matrix.os == 'windows-latest' - uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2 + uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0 - name: Generate key pair if: matrix.os == 'windows-latest' @@ -301,7 +301,7 @@ jobs: contents: write steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit @@ -310,7 +310,7 @@ jobs: - name: Display structure of downloaded files run: ls -R - name: Upload binaries, attestations and signatures to Release and create GitHub Release - uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2 + uses: softprops/action-gh-release@62c96d0c4e8a889135c1f3a25910db8dbe0e85f7 # v2.3.4 with: tag_name: v${{ needs.read_versions.outputs.version }} generate_release_notes: true diff --git a/.github/workflows/pre_commit.yml b/.github/workflows/pre_commit.yml index eccf235d1..acd489f5b 100644 --- a/.github/workflows/pre_commit.yml +++ b/.github/workflows/pre_commit.yml @@ -12,17 +12,21 @@ permissions: jobs: pre-commit: runs-on: ubuntu-latest + env: + # Prevents sdist builds → no tar extraction + PIP_ONLY_BINARY: ":all:" + PIP_DISABLE_PIP_VERSION_CHECK: "1" permissions: contents: write pull-requests: write steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - name: Checkout repository - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 0 @@ -34,7 +38,7 @@ jobs: private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - name: Set up Python - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: python-version: 3.12 cache: 'pip' # caching pip dependencies @@ -42,13 +46,13 @@ jobs: - name: Run Pre-Commit Hooks run: | - pip install --require-hashes -r ./.github/scripts/requirements_pre_commit.txt + pip install --require-hashes --only-binary=:all: -r ./.github/scripts/requirements_pre_commit.txt - run: pre-commit run --all-files -c .pre-commit-config.yaml continue-on-error: true - name: Set up JDK - uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 + uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0 with: java-version: 17 distribution: "temurin" diff --git a/.github/workflows/push-docker.yml b/.github/workflows/push-docker.yml index 9a583c7b9..48e351433 100644 --- a/.github/workflows/push-docker.yml +++ b/.github/workflows/push-docker.yml @@ -30,19 +30,19 @@ jobs: id-token: write steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Set up JDK 17 - uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 + uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0 with: java-version: "17" distribution: "temurin" - - uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2 + - uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0 with: gradle-version: 8.14 @@ -54,7 +54,7 @@ jobs: - name: Install cosign if: github.ref == 'refs/heads/master' - uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2 + uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0 with: cosign-release: "v2.4.1" @@ -67,13 +67,13 @@ jobs: run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT - name: Login to Docker Hub - uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 + uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_API }} - name: Login to GitHub Container Registry - uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 + uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 with: registry: ghcr.io username: ${{ github.actor }} diff --git a/.github/workflows/releaseArtifacts.yml b/.github/workflows/releaseArtifacts.yml index 7839ffd64..faedf892d 100644 --- a/.github/workflows/releaseArtifacts.yml +++ b/.github/workflows/releaseArtifacts.yml @@ -23,19 +23,19 @@ jobs: version: ${{ steps.versionNumber.outputs.versionNumber }} steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Set up JDK 17 - uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 + uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0 with: java-version: "17" distribution: "temurin" - - uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2 + - uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0 with: gradle-version: 8.14 @@ -83,7 +83,7 @@ jobs: file_suffix: "" steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit @@ -95,7 +95,7 @@ jobs: run: ls -R - name: Install Cosign - uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2 + uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0 - name: Generate key pair run: cosign generate-key-pair @@ -161,7 +161,7 @@ jobs: file_suffix: "" steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit @@ -171,7 +171,7 @@ jobs: name: signed${{ matrix.file_suffix }} - name: Upload binaries, attestations and signatures to Release and create GitHub Release - uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2 + uses: softprops/action-gh-release@62c96d0c4e8a889135c1f3a25910db8dbe0e85f7 # v2.3.4 with: tag_name: v${{ needs.build.outputs.version }} generate_release_notes: true diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index a3a355845..ca037b7c0 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -34,17 +34,17 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - name: "Checkout code" - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2 + uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 with: results_file: results.sarif results_format: sarif @@ -74,6 +74,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@76621b61decf072c1cee8dd1ce2d2a82d33c17ed # v3.29.5 + uses: github/codeql-action/upload-sarif@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.29.5 with: sarif_file: results.sarif diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index 1e0e3ec32..146eb4b39 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -30,16 +30,16 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 0 - name: Setup Gradle - uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2 + uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0 - name: Build and analyze with Gradle env: diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 88b150e29..c3c0b110a 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -16,12 +16,12 @@ jobs: pull-requests: write steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - name: 30 days stale issues - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0 + uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 30 diff --git a/.github/workflows/swagger.yml b/.github/workflows/swagger.yml index ebb51704c..16f0a3088 100644 --- a/.github/workflows/swagger.yml +++ b/.github/workflows/swagger.yml @@ -26,19 +26,19 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Set up JDK 17 - uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 + uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0 with: java-version: "17" distribution: "temurin" - - uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2 + - uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0 - name: Generate Swagger documentation run: ./gradlew :stirling-pdf:generateOpenApiDocs diff --git a/.github/workflows/sync_files.yml b/.github/workflows/sync_files.yml index d2ff7e827..1233ac701 100644 --- a/.github/workflows/sync_files.yml +++ b/.github/workflows/sync_files.yml @@ -30,13 +30,17 @@ permissions: jobs: sync-files: runs-on: ubuntu-latest + env: + # Prevents sdist builds → no tar extraction + PIP_ONLY_BINARY: ":all:" + PIP_DISABLE_PIP_VERSION_CHECK: "1" steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup GitHub App Bot id: setup-bot @@ -46,7 +50,7 @@ jobs: private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - name: Set up Python - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: python-version: "3.12" cache: "pip" # caching pip dependencies @@ -61,7 +65,9 @@ jobs: git diff --staged --quiet || git commit -m ":memo: Sync translation files" || echo "No changes detected" - name: Install dependencies - run: pip install --require-hashes -r ./.github/scripts/requirements_sync_readme.txt + # Wheels-only + Hash-Pinning + run: | + pip install --require-hashes --only-binary=:all: -r ./.github/scripts/requirements_sync_readme.txt - name: Sync README.md run: | diff --git a/.github/workflows/testdriver.yml b/.github/workflows/testdriver.yml index 209ce7435..2bd47bee3 100644 --- a/.github/workflows/testdriver.yml +++ b/.github/workflows/testdriver.yml @@ -24,21 +24,21 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - name: Checkout repository - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Set up JDK - uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 + uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0 with: java-version: '17' distribution: 'temurin' - name: Setup Gradle - uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2 + uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0 with: gradle-version: 8.14 @@ -57,7 +57,7 @@ jobs: echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT - name: Login to Docker Hub - uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 + uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_API }} @@ -116,28 +116,47 @@ jobs: docker-compose up -d EOF + files-changed: + if: always() + name: detect what files changed + runs-on: ubuntu-latest + timeout-minutes: 3 + outputs: + frontend: ${{ steps.changes.outputs.frontend }} + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + + - name: Check for file changes + uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 + id: changes + with: + filters: ".github/config/.files.yaml" + test: - needs: deploy + if: needs.files-changed.outputs.frontend == 'true' + needs: [deploy, files-changed] runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit - - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Set up Node - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 with: cache: 'npm' + cache-dependency-path: frontend/package-lock.json - name: Run TestDriver.ai uses: testdriverai/action@f0d0f45fdd684db628baa843fe9313f3ca3a8aa8 #1.1.3 with: key: ${{secrets.TESTDRIVER_API_KEY}} prerun: | + cd frontend npm install npm run build npm install dashcam-chrome --save @@ -156,7 +175,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit @@ -167,6 +186,7 @@ jobs: sudo chmod 600 ../private.key - name: Cleanup deployment + if: always() run: | ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} << EOF cd /stirling/test-${{ github.sha }} @@ -174,3 +194,4 @@ jobs: cd /stirling rm -rf test-${{ github.sha }} EOF + continue-on-error: true # Ensure cleanup runs even if previous steps fail diff --git a/.gitignore b/.gitignore index b339d7ff6..3d9b2a949 100644 --- a/.gitignore +++ b/.gitignore @@ -204,6 +204,9 @@ id_ed25519.pub # node_modules node_modules/ +# weasyPrint +**/LOCAL_APPDATA_FONTCONFIG_CACHE/** + # Translation temp files *_compact.json *compact*.json diff --git a/DATABASE.md b/DATABASE.md index c37c598bb..5c245af27 100644 --- a/DATABASE.md +++ b/DATABASE.md @@ -5,7 +5,7 @@ The newly introduced feature enhances the application with robust database backup and import capabilities. This feature is designed to ensure data integrity and provide a straightforward way to manage database backups. Here's how it works: 1. Automatic Backup Creation - - The system automatically creates a database backup every day at midnight. This ensures that there is always a recent backup available, minimizing the risk of data loss. + - The system automatically creates a database backup on a configurable schedule (default: daily at midnight via `system.databaseBackup.cron`). This ensures that there is always a recent backup available, minimizing the risk of data loss. 2. Manual Backup Export - Admin actions that modify the user database trigger a manual export of the database. This keeps the backup up-to-date with the latest changes and provides an extra layer of data security. 3. Importing Database Backups diff --git a/app/allowed-licenses.json b/app/allowed-licenses.json index 80e919439..830ae037a 100644 --- a/app/allowed-licenses.json +++ b/app/allowed-licenses.json @@ -167,6 +167,10 @@ { "moduleName": ".*", "moduleLicense": "The W3C License" + }, + { + "moduleName": ".*", + "moduleLicense": "UnRar License" } ] } diff --git a/app/common/build.gradle b/app/common/build.gradle index f79ec6982..0a5a37c6e 100644 --- a/app/common/build.gradle +++ b/app/common/build.gradle @@ -33,13 +33,14 @@ dependencies { api 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20240325.1' api 'com.fathzer:javaluator:3.0.6' api 'com.posthog.java:posthog:1.2.0' - api 'org.apache.commons:commons-lang3:3.18.0' + api 'org.apache.commons:commons-lang3:3.19.0' api 'com.drewnoakes:metadata-extractor:2.19.0' // Image metadata extractor api 'com.vladsch.flexmark:flexmark-html2md-converter:0.64.8' api "org.apache.pdfbox:pdfbox:$pdfboxVersion" + api 'com.github.junrar:junrar:7.5.5' // RAR archive support for CBR files api 'jakarta.servlet:jakarta.servlet-api:6.1.0' api 'org.snakeyaml:snakeyaml-engine:2.10' - api "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.9" - api 'jakarta.mail:jakarta.mail-api:2.1.3' - runtimeOnly 'org.eclipse.angus:angus-mail:2.0.4' + api "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.13" + api 'jakarta.mail:jakarta.mail-api:2.1.5' + runtimeOnly 'org.eclipse.angus:angus-mail:2.0.5' } diff --git a/app/common/src/main/java/stirling/software/common/annotations/AutoJobPostMapping.java b/app/common/src/main/java/stirling/software/common/annotations/AutoJobPostMapping.java index 48ea489d6..766d605b2 100644 --- a/app/common/src/main/java/stirling/software/common/annotations/AutoJobPostMapping.java +++ b/app/common/src/main/java/stirling/software/common/annotations/AutoJobPostMapping.java @@ -3,6 +3,7 @@ package stirling.software.common.annotations; import java.lang.annotation.*; import org.springframework.core.annotation.AliasFor; +import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @@ -40,7 +41,7 @@ public @interface AutoJobPostMapping { /** MIME types this endpoint accepts. Defaults to {@code multipart/form-data}. */ @AliasFor(annotation = RequestMapping.class, attribute = "consumes") - String[] consumes() default {"multipart/form-data"}; + String[] consumes() default {MediaType.MULTIPART_FORM_DATA_VALUE}; /** * Maximum execution time in milliseconds before the job is aborted. A negative value means "use diff --git a/app/common/src/main/java/stirling/software/common/aop/AutoJobAspect.java b/app/common/src/main/java/stirling/software/common/aop/AutoJobAspect.java index 2ee10ebcd..ac36cd0d7 100644 --- a/app/common/src/main/java/stirling/software/common/aop/AutoJobAspect.java +++ b/app/common/src/main/java/stirling/software/common/aop/AutoJobAspect.java @@ -19,7 +19,6 @@ import lombok.extern.slf4j.Slf4j; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.model.api.PDFFile; -import stirling.software.common.service.FileOrUploadService; import stirling.software.common.service.FileStorage; import stirling.software.common.service.JobExecutorService; @@ -34,7 +33,6 @@ public class AutoJobAspect { private final JobExecutorService jobExecutorService; private final HttpServletRequest request; - private final FileOrUploadService fileOrUploadService; private final FileStorage fileStorage; @Around("@annotation(autoJobPostMapping)") @@ -53,7 +51,8 @@ public class AutoJobAspect { boolean trackProgress = autoJobPostMapping.trackProgress(); log.debug( - "AutoJobPostMapping execution with async={}, timeout={}, retryCount={}, trackProgress={}", + "AutoJobPostMapping execution with async={}, timeout={}, retryCount={}," + + " trackProgress={}", async, timeout > 0 ? timeout : "default", retryCount, @@ -148,7 +147,8 @@ public class AutoJobAspect { } catch (Throwable ex) { lastException = ex; log.error( - "AutoJobAspect caught exception during job execution (attempt {}/{}): {}", + "AutoJobAspect caught exception during job execution (attempt" + + " {}/{}): {}", currentAttempt, maxRetries, ex.getMessage(), diff --git a/app/common/src/main/java/stirling/software/common/configuration/ConfigInitializer.java b/app/common/src/main/java/stirling/software/common/configuration/ConfigInitializer.java index 50090ee51..54e42504c 100644 --- a/app/common/src/main/java/stirling/software/common/configuration/ConfigInitializer.java +++ b/app/common/src/main/java/stirling/software/common/configuration/ConfigInitializer.java @@ -23,10 +23,30 @@ import stirling.software.common.util.YamlHelper; @Slf4j public class ConfigInitializer { + private static final int MIN_SETTINGS_FILE_LINES = 31; + public void ensureConfigExists() throws IOException, URISyntaxException { // 1) If settings file doesn't exist, create from template Path destPath = Paths.get(InstallationPathConfig.getSettingsPath()); - if (Files.notExists(destPath)) { + + boolean settingsFileExists = Files.exists(destPath); + + long lineCount = settingsFileExists ? Files.readAllLines(destPath).size() : 0; + + log.info("Current settings file line count: {}", lineCount); + + if (!settingsFileExists || lineCount < MIN_SETTINGS_FILE_LINES) { + if (settingsFileExists) { + // move settings.yml to settings.yml.{timestamp}.bak + Path backupPath = + Paths.get( + InstallationPathConfig.getSettingsPath() + + "." + + System.currentTimeMillis() + + ".bak"); + Files.move(destPath, backupPath, StandardCopyOption.REPLACE_EXISTING); + log.info("Moved existing settings file to backup: {}", backupPath); + } Files.createDirectories(destPath.getParent()); try (InputStream in = getClass().getClassLoader().getResourceAsStream("settings.yml.template")) { diff --git a/app/common/src/main/java/stirling/software/common/configuration/InstallationPathConfig.java b/app/common/src/main/java/stirling/software/common/configuration/InstallationPathConfig.java index 64fbc41b7..db00c6960 100644 --- a/app/common/src/main/java/stirling/software/common/configuration/InstallationPathConfig.java +++ b/app/common/src/main/java/stirling/software/common/configuration/InstallationPathConfig.java @@ -14,12 +14,17 @@ public class InstallationPathConfig { private static final String CONFIG_PATH; private static final String CUSTOM_FILES_PATH; private static final String CLIENT_WEBUI_PATH; - private static final String SCRIPTS_PATH; private static final String PIPELINE_PATH; // Config paths private static final String SETTINGS_PATH; private static final String CUSTOM_SETTINGS_PATH; + private static final String SCRIPTS_PATH; + private static final String BACKUP_PATH; + + // Backup paths + private static final String BACKUP_DB_PATH; + private static final String BACKUP_PRIVATE_KEY_PATH; // Custom file paths private static final String STATIC_PATH; @@ -41,6 +46,11 @@ public class InstallationPathConfig { SETTINGS_PATH = CONFIG_PATH + "settings.yml"; CUSTOM_SETTINGS_PATH = CONFIG_PATH + "custom_settings.yml"; SCRIPTS_PATH = CONFIG_PATH + "scripts" + File.separator; + BACKUP_PATH = CONFIG_PATH + "backup" + File.separator; + + // Initialize backup paths + BACKUP_DB_PATH = BACKUP_PATH + "db" + File.separator; + BACKUP_PRIVATE_KEY_PATH = BACKUP_PATH + "keys" + File.separator; // Initialize custom file paths STATIC_PATH = CUSTOM_FILES_PATH + "static" + File.separator; @@ -124,6 +134,10 @@ public class InstallationPathConfig { } public static String getPrivateKeyPath() { - return PRIVATE_KEY_PATH; + return BACKUP_PRIVATE_KEY_PATH; + } + + public static String getBackupPath() { + return BACKUP_DB_PATH; } } diff --git a/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java b/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java index 2cc988cfa..4b5a202c0 100644 --- a/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java +++ b/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java @@ -41,6 +41,7 @@ import stirling.software.common.model.oauth2.GitHubProvider; import stirling.software.common.model.oauth2.GoogleProvider; import stirling.software.common.model.oauth2.KeycloakProvider; import stirling.software.common.model.oauth2.Provider; +import stirling.software.common.service.SsrfProtectionService.SsrfProtectionLevel; import stirling.software.common.util.ValidationUtils; @Data @@ -72,11 +73,11 @@ public class ApplicationProperties { public PropertySource dynamicYamlPropertySource(ConfigurableEnvironment environment) throws IOException { String configPath = InstallationPathConfig.getSettingsPath(); - log.debug("Attempting to load settings from: " + configPath); + log.debug("Attempting to load settings from: {}", configPath); File file = new File(configPath); if (!file.exists()) { - log.error("Warning: Settings file does not exist at: " + configPath); + log.error("Warning: Settings file does not exist at: {}", configPath); } Resource resource = new FileSystemResource(configPath); @@ -89,7 +90,7 @@ public class ApplicationProperties { new YamlPropertySourceFactory().createPropertySource(null, encodedResource); environment.getPropertySources().addFirst(propertySource); - log.debug("Loaded properties: " + propertySource.getSource()); + log.debug("Loaded properties: {}", propertySource.getSource()); return propertySource; } @@ -365,6 +366,7 @@ public class ApplicationProperties { private CustomPaths customPaths = new CustomPaths(); private String fileUploadLimit; private TempFileManagement tempFileManagement = new TempFileManagement(); + private DatabaseBackup databaseBackup = new DatabaseBackup(); private List corsAllowedOrigins = new ArrayList<>(); private String frontendUrl; // Base URL for frontend (used for invite links, etc.). If not set, @@ -388,6 +390,11 @@ public class ApplicationProperties { } } + @Data + public static class DatabaseBackup { + private String cron = "0 0 0 * * ?"; // daily at midnight + } + @Data public static class CustomPaths { private Pipeline pipeline = new Pipeline(); @@ -424,17 +431,19 @@ public class ApplicationProperties { @JsonIgnore public String getBaseTmpDir() { - return baseTmpDir != null && !baseTmpDir.isEmpty() - ? baseTmpDir - : java.lang.System.getProperty("java.io.tmpdir").replaceAll("/+$", "") - + "/stirling-pdf"; + if (baseTmpDir != null && !baseTmpDir.isEmpty()) { + return baseTmpDir; + } + String tmp = java.lang.System.getProperty("java.io.tmpdir"); + return new File(tmp, "stirling-pdf").getPath(); } @JsonIgnore public String getLibreofficeDir() { - return libreofficeDir != null && !libreofficeDir.isEmpty() - ? libreofficeDir - : getBaseTmpDir() + "/libreoffice"; + if (libreofficeDir != null && !libreofficeDir.isEmpty()) { + return libreofficeDir; + } + return new File(getBaseTmpDir(), "libreoffice").getPath(); } } @@ -445,7 +454,7 @@ public class ApplicationProperties { @Data public static class UrlSecurity { private boolean enabled = true; - private String level = "MEDIUM"; // MAX, MEDIUM, OFF + private SsrfProtectionLevel level = SsrfProtectionLevel.MEDIUM; // MAX, MEDIUM, OFF private List allowedDomains = new ArrayList<>(); private List blockedDomains = new ArrayList<>(); private List internalTlds = @@ -498,9 +507,7 @@ public class ApplicationProperties { private List languages; public String getAppNameNavbar() { - return appNameNavbar != null && appNameNavbar.trim().length() > 0 - ? appNameNavbar - : null; + return appNameNavbar != null && !appNameNavbar.trim().isEmpty() ? appNameNavbar : null; } } diff --git a/app/common/src/main/java/stirling/software/common/model/FileInfo.java b/app/common/src/main/java/stirling/software/common/model/FileInfo.java index 41a3a4717..2e3e59e83 100644 --- a/app/common/src/main/java/stirling/software/common/model/FileInfo.java +++ b/app/common/src/main/java/stirling/software/common/model/FileInfo.java @@ -8,9 +8,11 @@ import java.util.Locale; import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; -@AllArgsConstructor @Data +@NoArgsConstructor +@AllArgsConstructor public class FileInfo { private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); diff --git a/app/common/src/main/java/stirling/software/common/model/InputStreamTemplateResource.java b/app/common/src/main/java/stirling/software/common/model/InputStreamTemplateResource.java index 3bac16fef..92d7418ea 100644 --- a/app/common/src/main/java/stirling/software/common/model/InputStreamTemplateResource.java +++ b/app/common/src/main/java/stirling/software/common/model/InputStreamTemplateResource.java @@ -4,14 +4,12 @@ package stirling.software.common.model; * This class will be removed when frontend migration to React is complete -public class InputStreamTemplateResource implements ITemplateResource { - private InputStream inputStream; - private String characterEncoding; - public InputStreamTemplateResource(InputStream inputStream, String characterEncoding) { - this.inputStream = inputStream; - this.characterEncoding = characterEncoding; - } +@RequiredArgsConstructor +@Getter +public class InputStreamTemplateResource implements ITemplateResource { + private final InputStream inputStream; + private final String characterEncoding; @Override public Reader reader() throws IOException { diff --git a/app/common/src/main/java/stirling/software/common/model/PdfMetadata.java b/app/common/src/main/java/stirling/software/common/model/PdfMetadata.java index ef8684788..7991300d7 100644 --- a/app/common/src/main/java/stirling/software/common/model/PdfMetadata.java +++ b/app/common/src/main/java/stirling/software/common/model/PdfMetadata.java @@ -1,12 +1,16 @@ package stirling.software.common.model; -import java.util.Calendar; +import java.time.ZonedDateTime; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class PdfMetadata { private String author; private String producer; @@ -14,6 +18,6 @@ public class PdfMetadata { private String creator; private String subject; private String keywords; - private Calendar creationDate; - private Calendar modificationDate; + private ZonedDateTime creationDate; + private ZonedDateTime modificationDate; } diff --git a/app/common/src/main/java/stirling/software/common/model/api/PDFFile.java b/app/common/src/main/java/stirling/software/common/model/api/PDFFile.java index b584fde2f..f999f7dc0 100644 --- a/app/common/src/main/java/stirling/software/common/model/api/PDFFile.java +++ b/app/common/src/main/java/stirling/software/common/model/api/PDFFile.java @@ -1,5 +1,6 @@ package stirling.software.common.model.api; +import org.springframework.http.MediaType; import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.media.Schema; @@ -15,7 +16,11 @@ import lombok.NoArgsConstructor; @EqualsAndHashCode @Schema(description = "PDF file input - either upload a file or provide a server-side file ID") public class PDFFile { - @Schema(description = "The input PDF file", format = "binary") + + @Schema( + description = "The input PDF file", + contentMediaType = MediaType.APPLICATION_PDF_VALUE, + format = "binary") private MultipartFile fileInput; @Schema( diff --git a/app/common/src/main/java/stirling/software/common/model/api/misc/ReplaceAndInvert.java b/app/common/src/main/java/stirling/software/common/model/api/misc/ReplaceAndInvert.java index f9cbaace1..1f421375a 100644 --- a/app/common/src/main/java/stirling/software/common/model/api/misc/ReplaceAndInvert.java +++ b/app/common/src/main/java/stirling/software/common/model/api/misc/ReplaceAndInvert.java @@ -4,4 +4,5 @@ public enum ReplaceAndInvert { HIGH_CONTRAST_COLOR, CUSTOM_COLOR, FULL_INVERSION, + COLOR_SPACE_CONVERSION, } diff --git a/app/common/src/main/java/stirling/software/common/service/JobExecutorService.java b/app/common/src/main/java/stirling/software/common/service/JobExecutorService.java index 73afa22a0..6f595857a 100644 --- a/app/common/src/main/java/stirling/software/common/service/JobExecutorService.java +++ b/app/common/src/main/java/stirling/software/common/service/JobExecutorService.java @@ -22,6 +22,7 @@ import lombok.extern.slf4j.Slf4j; import stirling.software.common.model.job.JobResponse; import stirling.software.common.util.ExecutorFactory; +import stirling.software.common.util.RegexPatternUtils; /** Service for executing jobs asynchronously or synchronously */ @Service @@ -227,7 +228,8 @@ public class JobExecutorService { if (result instanceof byte[]) { // Store byte array directly to disk to avoid double memory consumption String fileId = fileStorage.storeBytes((byte[]) result, "result.pdf"); - taskManager.setFileResult(jobId, fileId, "result.pdf", "application/pdf"); + taskManager.setFileResult( + jobId, fileId, "result.pdf", MediaType.APPLICATION_PDF_VALUE); log.debug("Stored byte[] result with fileId: {}", fileId); // Let the byte array get collected naturally in the next GC cycle @@ -239,7 +241,7 @@ public class JobExecutorService { if (body instanceof byte[]) { // Extract filename from content-disposition header if available String filename = "result.pdf"; - String contentType = "application/pdf"; + String contentType = MediaType.APPLICATION_PDF_VALUE; if (response.getHeaders().getContentDisposition() != null) { String disposition = @@ -252,8 +254,10 @@ public class JobExecutorService { } } - if (response.getHeaders().getContentType() != null) { - contentType = response.getHeaders().getContentType().toString(); + MediaType mediaType = response.getHeaders().getContentType(); + + if (mediaType != null) { + contentType = mediaType.toString(); } // Store byte array directly to disk @@ -274,7 +278,7 @@ public class JobExecutorService { if (fileId != null && !fileId.isEmpty()) { // Try to get filename and content type String filename = "result.pdf"; - String contentType = "application/pdf"; + String contentType = MediaType.APPLICATION_PDF_VALUE; try { java.lang.reflect.Method getOriginalFileName = @@ -315,8 +319,7 @@ public class JobExecutorService { // Store generic result taskManager.setResult(jobId, body); } - } else if (result instanceof MultipartFile) { - MultipartFile file = (MultipartFile) result; + } else if (result instanceof MultipartFile file) { String fileId = fileStorage.storeFile(file); taskManager.setFileResult( jobId, fileId, file.getOriginalFilename(), file.getContentType()); @@ -333,7 +336,7 @@ public class JobExecutorService { if (fileId != null && !fileId.isEmpty()) { // Try to get filename and content type String filename = "result.pdf"; - String contentType = "application/pdf"; + String contentType = MediaType.APPLICATION_PDF_VALUE; try { java.lang.reflect.Method getOriginalFileName = @@ -396,9 +399,8 @@ public class JobExecutorService { HttpHeaders.CONTENT_DISPOSITION, "form-data; name=\"attachment\"; filename=\"result.pdf\"") .body(result); - } else if (result instanceof MultipartFile) { + } else if (result instanceof MultipartFile file) { // Return MultipartFile content - MultipartFile file = (MultipartFile) result; return ResponseEntity.ok() .contentType(MediaType.parseMediaType(file.getContentType())) .header( @@ -425,8 +427,16 @@ public class JobExecutorService { } try { - String value = timeout.replaceAll("[^\\d.]", ""); - String unit = timeout.replaceAll("[\\d.]", ""); + String value = + RegexPatternUtils.getInstance() + .getNonDigitDotPattern() + .matcher(timeout) + .replaceAll(""); + String unit = + RegexPatternUtils.getInstance() + .getDigitDotPattern() + .matcher(timeout) + .replaceAll(""); double numericValue = Double.parseDouble(value); diff --git a/app/common/src/main/java/stirling/software/common/service/PdfMetadataService.java b/app/common/src/main/java/stirling/software/common/service/PdfMetadataService.java index 621e19d46..0d2eebc10 100644 --- a/app/common/src/main/java/stirling/software/common/service/PdfMetadataService.java +++ b/app/common/src/main/java/stirling/software/common/service/PdfMetadataService.java @@ -1,5 +1,9 @@ package stirling.software.common.service; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; import java.util.Calendar; import org.apache.pdfbox.pdmodel.PDDocument; @@ -29,17 +33,19 @@ public class PdfMetadataService { this.runningProOrHigher = runningProOrHigher; } - public PdfMetadata extractMetadataFromPdf(PDDocument pdf) { - return PdfMetadata.builder() - .author(pdf.getDocumentInformation().getAuthor()) - .producer(pdf.getDocumentInformation().getProducer()) - .title(pdf.getDocumentInformation().getTitle()) - .creator(pdf.getDocumentInformation().getCreator()) - .subject(pdf.getDocumentInformation().getSubject()) - .keywords(pdf.getDocumentInformation().getKeywords()) - .creationDate(pdf.getDocumentInformation().getCreationDate()) - .modificationDate(pdf.getDocumentInformation().getModificationDate()) - .build(); + /** + * Converts ZonedDateTime to Calendar for PDFBox compatibility. + * + * @param zonedDateTime the ZonedDateTime to convert + * @return Calendar instance or null if input is null + */ + public static Calendar toCalendar(ZonedDateTime zonedDateTime) { + if (zonedDateTime == null) { + return null; + } + Calendar calendar = Calendar.getInstance(); + calendar.setTimeInMillis(zonedDateTime.toInstant().toEpochMilli()); + return calendar; } public void setDefaultMetadata(PDDocument pdf) { @@ -58,6 +64,52 @@ public class PdfMetadataService { setCommonMetadata(pdf, pdfMetadata); } + /** + * Parses a date string and converts it to Calendar for PDFBox compatibility. + * + * @param dateString the date string in "yyyy/MM/dd HH:mm:ss" format + * @return Calendar instance or null if parsing fails or input is empty + */ + public static Calendar parseToCalendar(String dateString) { + if (dateString == null || dateString.trim().isEmpty()) { + return null; + } + try { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss"); + ZonedDateTime zonedDateTime = + LocalDateTime.parse(dateString, formatter).atZone(ZoneId.systemDefault()); + return toCalendar(zonedDateTime); + } catch (Exception e) { + return null; + } + } + + public PdfMetadata extractMetadataFromPdf(PDDocument pdf) { + Calendar creationCal = pdf.getDocumentInformation().getCreationDate(); + Calendar modificationCal = pdf.getDocumentInformation().getModificationDate(); + + ZonedDateTime creationDate = + creationCal != null + ? ZonedDateTime.ofInstant(creationCal.toInstant(), ZoneId.systemDefault()) + : null; + ZonedDateTime modificationDate = + modificationCal != null + ? ZonedDateTime.ofInstant( + modificationCal.toInstant(), ZoneId.systemDefault()) + : null; + + return PdfMetadata.builder() + .author(pdf.getDocumentInformation().getAuthor()) + .producer(pdf.getDocumentInformation().getProducer()) + .title(pdf.getDocumentInformation().getTitle()) + .creator(pdf.getDocumentInformation().getCreator()) + .subject(pdf.getDocumentInformation().getSubject()) + .keywords(pdf.getDocumentInformation().getKeywords()) + .creationDate(creationDate) + .modificationDate(modificationDate) + .build(); + } + private void setNewDocumentMetadata(PDDocument pdf, PdfMetadata pdfMetadata) { String creator = stirlingPDFLabel; @@ -79,7 +131,13 @@ public class PdfMetadataService { } pdf.getDocumentInformation().setCreator(creator); - pdf.getDocumentInformation().setCreationDate(Calendar.getInstance()); + + // Use existing creation date if available, otherwise create new one + Calendar creationCal = + pdfMetadata.getCreationDate() != null + ? toCalendar(pdfMetadata.getCreationDate()) + : Calendar.getInstance(); + pdf.getDocumentInformation().setCreationDate(creationCal); } private void setCommonMetadata(PDDocument pdf, PdfMetadata pdfMetadata) { @@ -88,7 +146,13 @@ public class PdfMetadataService { pdf.getDocumentInformation().setProducer(stirlingPDFLabel); pdf.getDocumentInformation().setSubject(pdfMetadata.getSubject()); pdf.getDocumentInformation().setKeywords(pdfMetadata.getKeywords()); - pdf.getDocumentInformation().setModificationDate(Calendar.getInstance()); + + // Convert ZonedDateTime to Calendar for PDFBox compatibility + Calendar modificationCal = + pdfMetadata.getModificationDate() != null + ? toCalendar(pdfMetadata.getModificationDate()) + : Calendar.getInstance(); + pdf.getDocumentInformation().setModificationDate(modificationCal); String author = pdfMetadata.getAuthor(); if (applicationProperties diff --git a/app/common/src/main/java/stirling/software/common/service/SsrfProtectionService.java b/app/common/src/main/java/stirling/software/common/service/SsrfProtectionService.java index 97c2da12e..1f81fb4d4 100644 --- a/app/common/src/main/java/stirling/software/common/service/SsrfProtectionService.java +++ b/app/common/src/main/java/stirling/software/common/service/SsrfProtectionService.java @@ -1,5 +1,7 @@ package stirling.software.common.service; +import java.net.Inet4Address; +import java.net.Inet6Address; import java.net.InetAddress; import java.net.URI; import java.net.UnknownHostException; @@ -11,6 +13,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import stirling.software.common.model.ApplicationProperties; +import stirling.software.common.util.RegexPatternUtils; @Service @RequiredArgsConstructor @@ -20,8 +23,9 @@ public class SsrfProtectionService { private final ApplicationProperties applicationProperties; private static final Pattern DATA_URL_PATTERN = - Pattern.compile("^data:.*", Pattern.CASE_INSENSITIVE); - private static final Pattern FRAGMENT_PATTERN = Pattern.compile("^#.*"); + RegexPatternUtils.getInstance().getPattern("^data:.*", Pattern.CASE_INSENSITIVE); + private static final Pattern FRAGMENT_PATTERN = + RegexPatternUtils.getInstance().getPattern("^#.*"); public enum SsrfProtectionLevel { OFF, // No SSRF protection - allows all URLs @@ -51,21 +55,17 @@ public class SsrfProtectionService { SsrfProtectionLevel level = parseProtectionLevel(config.getLevel()); - switch (level) { - case OFF: - return true; - case MAX: - return isMaxSecurityAllowed(trimmedUrl, config); - case MEDIUM: - return isMediumSecurityAllowed(trimmedUrl, config); - default: - return false; - } + return switch (level) { + case OFF -> true; + case MAX -> isMaxSecurityAllowed(trimmedUrl, config); + case MEDIUM -> isMediumSecurityAllowed(trimmedUrl, config); + default -> false; + }; } - private SsrfProtectionLevel parseProtectionLevel(String level) { + private SsrfProtectionLevel parseProtectionLevel(SsrfProtectionLevel level) { try { - return SsrfProtectionLevel.valueOf(level.toUpperCase()); + return SsrfProtectionLevel.valueOf(level.name()); } catch (IllegalArgumentException e) { log.warn("Invalid SSRF protection level '{}', defaulting to MEDIUM", level); return SsrfProtectionLevel.MEDIUM; @@ -172,15 +172,62 @@ public class SsrfProtectionService { } private boolean isPrivateAddress(InetAddress address) { - return address.isSiteLocalAddress() - || address.isAnyLocalAddress() - || isPrivateIPv4Range(address.getHostAddress()); + if (address.isAnyLocalAddress() || address.isLoopbackAddress()) { + return true; + } + + if (address instanceof Inet4Address) { + return isPrivateIPv4Range(address.getHostAddress()); + } + + if (address instanceof Inet6Address addr6) { + if (addr6.isLinkLocalAddress() || addr6.isSiteLocalAddress()) { + return true; + } + + byte[] bytes = addr6.getAddress(); + if (isIpv4MappedAddress(bytes)) { + String ipv4 = + (bytes[12] & 0xff) + + "." + + (bytes[13] & 0xff) + + "." + + (bytes[14] & 0xff) + + "." + + (bytes[15] & 0xff); + return isPrivateIPv4Range(ipv4); + } + + int firstByte = bytes[0] & 0xff; + // Check for IPv6 unique local addresses (fc00::/7) + if ((firstByte & 0xfe) == 0xfc) { + return true; + } + } + + return false; + } + + private boolean isIpv4MappedAddress(byte[] addr) { + if (addr.length != 16) { + return false; + } + for (int i = 0; i < 10; i++) { + if (addr[i] != 0) { + return false; + } + } + // For IPv4-mapped IPv6 addresses, bytes 10 and 11 must be 0xff (i.e., address is + // ::ffff:w.x.y.z) + return addr[10] == (byte) 0xff && addr[11] == (byte) 0xff; } private boolean isPrivateIPv4Range(String ip) { + // Includes RFC1918, loopback, link-local, and unspecified addresses return ip.startsWith("10.") || ip.startsWith("192.168.") || (ip.startsWith("172.") && isInRange172(ip)) + || ip.startsWith("169.254.") || ip.startsWith("127.") || "0.0.0.0".equals(ip); } @@ -192,17 +239,31 @@ public class SsrfProtectionService { int secondOctet = Integer.parseInt(parts[1]); return secondOctet >= 16 && secondOctet <= 31; } catch (NumberFormatException e) { - return false; } } return false; } private boolean isCloudMetadataAddress(String ip) { + String normalizedIp = normalizeIpv4MappedAddress(ip); // Cloud metadata endpoints for AWS, GCP, Azure, Oracle Cloud, and IBM Cloud - return ip.startsWith("169.254.169.254") // AWS/GCP/Azure - || ip.startsWith("fd00:ec2::254") // AWS IPv6 - || ip.startsWith("169.254.169.253") // Oracle Cloud - || ip.startsWith("169.254.169.250"); // IBM Cloud + return normalizedIp.startsWith("169.254.169.254") // AWS/GCP/Azure + || normalizedIp.startsWith("fd00:ec2::254") // AWS IPv6 + || normalizedIp.startsWith("169.254.169.253") // Oracle Cloud + || normalizedIp.startsWith("169.254.169.250"); // IBM Cloud + } + + private String normalizeIpv4MappedAddress(String ip) { + if (ip == null) { + return ""; + } + if (ip.startsWith("::ffff:")) { + return ip.substring(7); + } + int lastColon = ip.lastIndexOf(':'); + if (lastColon >= 0 && ip.indexOf('.') > lastColon) { + return ip.substring(lastColon + 1); + } + return ip; } } diff --git a/app/common/src/main/java/stirling/software/common/service/TempFileCleanupService.java b/app/common/src/main/java/stirling/software/common/service/TempFileCleanupService.java index df85a016b..81b45aed0 100644 --- a/app/common/src/main/java/stirling/software/common/service/TempFileCleanupService.java +++ b/app/common/src/main/java/stirling/software/common/service/TempFileCleanupService.java @@ -23,6 +23,7 @@ import lombok.extern.slf4j.Slf4j; import stirling.software.common.model.ApplicationProperties; import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.RegexPatternUtils; import stirling.software.common.util.TempFileManager; import stirling.software.common.util.TempFileRegistry; @@ -61,8 +62,14 @@ public class TempFileCleanupService { // File patterns that identify common system temp files private static final Predicate IS_SYSTEM_TEMP_FILE = fileName -> - fileName.matches("lu\\d+[a-z0-9]*\\.tmp") - || fileName.matches("ocr_process\\d+") + RegexPatternUtils.getInstance() + .getSystemTempFile1Pattern() + .matcher(fileName) + .matches() + || RegexPatternUtils.getInstance() + .getSystemTempFile2Pattern() + .matcher(fileName) + .matches() || (fileName.startsWith("tmp") && !fileName.contains("jetty")) || fileName.startsWith("OSL_PIPE_") || (fileName.endsWith(".tmp") && !fileName.contains("jetty")); diff --git a/app/common/src/main/java/stirling/software/common/util/CbrUtils.java b/app/common/src/main/java/stirling/software/common/util/CbrUtils.java new file mode 100644 index 000000000..429d22407 --- /dev/null +++ b/app/common/src/main/java/stirling/software/common/util/CbrUtils.java @@ -0,0 +1,258 @@ +package stirling.software.common.util; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +import org.apache.commons.io.FilenameUtils; +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.PDImageXObject; +import org.springframework.web.multipart.MultipartFile; + +import com.github.junrar.Archive; +import com.github.junrar.exception.CorruptHeaderException; +import com.github.junrar.exception.RarException; +import com.github.junrar.rarfile.FileHeader; + +import lombok.experimental.UtilityClass; +import lombok.extern.slf4j.Slf4j; + +import stirling.software.common.service.CustomPDFDocumentFactory; + +@Slf4j +@UtilityClass +public class CbrUtils { + + public byte[] convertCbrToPdf( + MultipartFile cbrFile, + CustomPDFDocumentFactory pdfDocumentFactory, + TempFileManager tempFileManager) + throws IOException { + return convertCbrToPdf(cbrFile, pdfDocumentFactory, tempFileManager, false); + } + + public byte[] convertCbrToPdf( + MultipartFile cbrFile, + CustomPDFDocumentFactory pdfDocumentFactory, + TempFileManager tempFileManager, + boolean optimizeForEbook) + throws IOException { + + validateCbrFile(cbrFile); + + try (TempFile tempFile = new TempFile(tempFileManager, ".cbr")) { + cbrFile.transferTo(tempFile.getFile()); + + try (PDDocument document = pdfDocumentFactory.createNewDocument()) { + + Archive archive; + try { + archive = new Archive(tempFile.getFile()); + } catch (CorruptHeaderException e) { + log.warn( + "Failed to open CBR/RAR archive due to corrupt header: {}", + e.getMessage()); + throw ExceptionUtils.createIllegalArgumentException( + "error.invalidFormat", + "Invalid or corrupted CBR/RAR archive. " + + "The file may be corrupted, use an unsupported RAR format (RAR5+), " + + "or may not be a valid RAR archive. " + + "Please ensure the file is a valid RAR archive."); + } catch (RarException e) { + log.warn("Failed to open CBR/RAR archive: {}", e.getMessage()); + String errorMessage; + String exMessage = e.getMessage() != null ? e.getMessage() : ""; + + if (exMessage.contains("encrypted")) { + errorMessage = "Encrypted CBR/RAR archives are not supported."; + } else if (exMessage.isEmpty()) { + errorMessage = + "Invalid CBR/RAR archive. " + + "The file may be encrypted, corrupted, or use an unsupported format."; + } else { + errorMessage = + "Invalid CBR/RAR archive: " + + exMessage + + ". The file may be encrypted, corrupted, or use an unsupported format."; + } + throw ExceptionUtils.createIllegalArgumentException( + "error.invalidFormat", errorMessage); + } catch (IOException e) { + log.warn("IO error reading CBR/RAR archive: {}", e.getMessage()); + throw ExceptionUtils.createFileProcessingException("CBR extraction", e); + } + + List imageEntries = new ArrayList<>(); + + try { + for (FileHeader fileHeader : archive) { + if (!fileHeader.isDirectory() && isImageFile(fileHeader.getFileName())) { + try (InputStream is = archive.getInputStream(fileHeader)) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + is.transferTo(baos); + imageEntries.add( + new ImageEntryData( + fileHeader.getFileName(), baos.toByteArray())); + } catch (Exception e) { + log.warn( + "Error reading image {}: {}", + fileHeader.getFileName(), + e.getMessage()); + } + } + } + } finally { + try { + archive.close(); + } catch (IOException e) { + log.warn("Error closing CBR/RAR archive: {}", e.getMessage()); + } + } + + imageEntries.sort( + Comparator.comparing(ImageEntryData::name, new NaturalOrderComparator())); + + if (imageEntries.isEmpty()) { + throw ExceptionUtils.createIllegalArgumentException( + "error.fileProcessing", + "No valid images found in the CBR file. The archive may be empty or contain no supported image formats."); + } + + for (ImageEntryData imageEntry : imageEntries) { + try { + PDImageXObject pdImage = + PDImageXObject.createFromByteArray( + document, imageEntry.data(), imageEntry.name()); + PDPage page = + new PDPage( + new PDRectangle(pdImage.getWidth(), pdImage.getHeight())); + document.addPage(page); + try (PDPageContentStream contentStream = + new PDPageContentStream(document, page)) { + contentStream.drawImage(pdImage, 0, 0); + } + } catch (IOException e) { + log.warn( + "Error processing image {}: {}", imageEntry.name(), e.getMessage()); + } + } + + if (document.getNumberOfPages() == 0) { + throw ExceptionUtils.createIllegalArgumentException( + "error.fileProcessing", + "No images could be processed from the CBR file. All images may be corrupted or in unsupported formats."); + } + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + document.save(baos); + byte[] pdfBytes = baos.toByteArray(); + + // Apply Ghostscript optimization if requested + if (optimizeForEbook) { + try { + return GeneralUtils.optimizePdfWithGhostscript(pdfBytes); + } catch (IOException e) { + log.warn("Ghostscript optimization failed, returning unoptimized PDF", e); + return pdfBytes; + } + } + + return pdfBytes; + } + } + } + + private void validateCbrFile(MultipartFile file) { + if (file == null || file.isEmpty()) { + throw new IllegalArgumentException("File cannot be null or empty"); + } + + String filename = file.getOriginalFilename(); + if (filename == null) { + throw new IllegalArgumentException("File must have a name"); + } + + String extension = FilenameUtils.getExtension(filename).toLowerCase(); + if (!"cbr".equals(extension) && !"rar".equals(extension)) { + throw new IllegalArgumentException("File must be a CBR or RAR archive"); + } + } + + public boolean isCbrFile(MultipartFile file) { + String filename = file.getOriginalFilename(); + if (filename == null) { + return false; + } + + String extension = FilenameUtils.getExtension(filename).toLowerCase(); + return "cbr".equals(extension) || "rar".equals(extension); + } + + private boolean isImageFile(String filename) { + return RegexPatternUtils.getInstance().getImageFilePattern().matcher(filename).matches(); + } + + private record ImageEntryData(String name, byte[] data) {} + + private class NaturalOrderComparator implements Comparator { + private static String getChunk(String s, int length, int marker) { + StringBuilder chunk = new StringBuilder(); + char c = s.charAt(marker); + chunk.append(c); + marker++; + + if (isDigit(c)) { + while (marker < length && isDigit(s.charAt(marker))) { + chunk.append(s.charAt(marker)); + marker++; + } + } else { + while (marker < length && !isDigit(s.charAt(marker))) { + chunk.append(s.charAt(marker)); + marker++; + } + } + return chunk.toString(); + } + + private static boolean isDigit(char ch) { + return ch >= '0' && ch <= '9'; + } + + @Override + public int compare(String s1, String s2) { + int len1 = s1.length(); + int len2 = s2.length(); + int marker1 = 0, marker2 = 0; + + while (marker1 < len1 && marker2 < len2) { + String chunk1 = getChunk(s1, len1, marker1); + marker1 += chunk1.length(); + + String chunk2 = getChunk(s2, len2, marker2); + marker2 += chunk2.length(); + + int result; + if (isDigit(chunk1.charAt(0)) && isDigit(chunk2.charAt(0))) { + int thisNumericValue = Integer.parseInt(chunk1); + int thatNumericValue = Integer.parseInt(chunk2); + result = Integer.compare(thisNumericValue, thatNumericValue); + } else { + result = chunk1.compareTo(chunk2); + } + + if (result != 0) { + return result; + } + } + + return Integer.compare(len1, len2); + } + } +} diff --git a/app/common/src/main/java/stirling/software/common/util/CbzUtils.java b/app/common/src/main/java/stirling/software/common/util/CbzUtils.java new file mode 100644 index 000000000..5eb620e8e --- /dev/null +++ b/app/common/src/main/java/stirling/software/common/util/CbzUtils.java @@ -0,0 +1,231 @@ +package stirling.software.common.util; + +import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Enumeration; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipInputStream; + +import org.apache.commons.io.FilenameUtils; +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.PDImageXObject; +import org.springframework.web.multipart.MultipartFile; + +import lombok.experimental.UtilityClass; +import lombok.extern.slf4j.Slf4j; + +import stirling.software.common.service.CustomPDFDocumentFactory; + +@Slf4j +@UtilityClass +public class CbzUtils { + + public byte[] convertCbzToPdf( + MultipartFile cbzFile, + CustomPDFDocumentFactory pdfDocumentFactory, + TempFileManager tempFileManager) + throws IOException { + return convertCbzToPdf(cbzFile, pdfDocumentFactory, tempFileManager, false); + } + + public byte[] convertCbzToPdf( + MultipartFile cbzFile, + CustomPDFDocumentFactory pdfDocumentFactory, + TempFileManager tempFileManager, + boolean optimizeForEbook) + throws IOException { + + validateCbzFile(cbzFile); + + try (TempFile tempFile = new TempFile(tempFileManager, ".cbz")) { + cbzFile.transferTo(tempFile.getFile()); + + // Early ZIP validity check using ZipInputStream (fail fast on non-zip content) + try (BufferedInputStream bis = + new BufferedInputStream( + new java.io.FileInputStream(tempFile.getFile())); + ZipInputStream zis = new ZipInputStream(bis)) { + if (zis.getNextEntry() == null) { + throw new IllegalArgumentException("Archive is empty or invalid ZIP"); + } + } catch (IOException e) { + throw new IllegalArgumentException("Invalid CBZ/ZIP archive", e); + } + + try (PDDocument document = pdfDocumentFactory.createNewDocument(); + ZipFile zipFile = new ZipFile(tempFile.getFile())) { + Enumeration entries = zipFile.entries(); + List imageEntries = new ArrayList<>(); + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + if (!entry.isDirectory() && isImageFile(entry.getName())) { + try (InputStream is = zipFile.getInputStream(entry)) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + is.transferTo(baos); + imageEntries.add( + new ImageEntryData(entry.getName(), baos.toByteArray())); + } catch (IOException e) { + log.warn("Error reading image {}: {}", entry.getName(), e.getMessage()); + } + } + } + + imageEntries.sort( + Comparator.comparing(ImageEntryData::name, new NaturalOrderComparator())); + + if (imageEntries.isEmpty()) { + throw new IllegalArgumentException("No valid images found in the CBZ file"); + } + + for (ImageEntryData imageEntry : imageEntries) { + try { + PDImageXObject pdImage = + PDImageXObject.createFromByteArray( + document, imageEntry.data(), imageEntry.name()); + PDPage page = + new PDPage( + new PDRectangle(pdImage.getWidth(), pdImage.getHeight())); + document.addPage(page); + try (PDPageContentStream contentStream = + new PDPageContentStream(document, page)) { + contentStream.drawImage(pdImage, 0, 0); + } + } catch (IOException e) { + log.warn( + "Error processing image {}: {}", imageEntry.name(), e.getMessage()); + } + } + + if (document.getNumberOfPages() == 0) { + throw new IllegalArgumentException( + "No images could be processed from the CBZ file"); + } + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + document.save(baos); + byte[] pdfBytes = baos.toByteArray(); + + // Apply Ghostscript optimization if requested + if (optimizeForEbook) { + try { + return GeneralUtils.optimizePdfWithGhostscript(pdfBytes); + } catch (IOException e) { + log.warn("Ghostscript optimization failed, returning unoptimized PDF", e); + return pdfBytes; + } + } + + return pdfBytes; + } + } + } + + private void validateCbzFile(MultipartFile file) { + if (file == null || file.isEmpty()) { + throw new IllegalArgumentException("File cannot be null or empty"); + } + + String filename = file.getOriginalFilename(); + if (filename == null) { + throw new IllegalArgumentException("File must have a name"); + } + + String extension = FilenameUtils.getExtension(filename).toLowerCase(); + if (!"cbz".equals(extension) && !"zip".equals(extension)) { + throw new IllegalArgumentException("File must be a CBZ or ZIP archive"); + } + } + + public boolean isCbzFile(MultipartFile file) { + String filename = file.getOriginalFilename(); + if (filename == null) { + return false; + } + + String extension = FilenameUtils.getExtension(filename).toLowerCase(); + return "cbz".equals(extension) || "zip".equals(extension); + } + + public static boolean isComicBookFile(MultipartFile file) { + String filename = file.getOriginalFilename(); + if (filename == null) { + return false; + } + + String extension = FilenameUtils.getExtension(filename).toLowerCase(); + return "cbz".equals(extension) + || "zip".equals(extension) + || "cbr".equals(extension) + || "rar".equals(extension); + } + + private boolean isImageFile(String filename) { + return RegexPatternUtils.getInstance().getImageFilePattern().matcher(filename).matches(); + } + + private record ImageEntryData(String name, byte[] data) {} + + private class NaturalOrderComparator implements Comparator { + @Override + public int compare(String s1, String s2) { + int len1 = s1.length(); + int len2 = s2.length(); + int marker1 = 0, marker2 = 0; + + while (marker1 < len1 && marker2 < len2) { + String chunk1 = getChunk(s1, len1, marker1); + marker1 += chunk1.length(); + + String chunk2 = getChunk(s2, len2, marker2); + marker2 += chunk2.length(); + + int result; + if (isDigit(chunk1.charAt(0)) && isDigit(chunk2.charAt(0))) { + int thisNumericValue = Integer.parseInt(chunk1); + int thatNumericValue = Integer.parseInt(chunk2); + result = Integer.compare(thisNumericValue, thatNumericValue); + } else { + result = chunk1.compareTo(chunk2); + } + + if (result != 0) { + return result; + } + } + + return Integer.compare(len1, len2); + } + + private static String getChunk(String s, int length, int marker) { + StringBuilder chunk = new StringBuilder(); + char c = s.charAt(marker); + chunk.append(c); + marker++; + + if (isDigit(c)) { + while (marker < length && isDigit(s.charAt(marker))) { + chunk.append(s.charAt(marker)); + marker++; + } + } else { + while (marker < length && !isDigit(s.charAt(marker))) { + chunk.append(s.charAt(marker)); + marker++; + } + } + return chunk.toString(); + } + + private static boolean isDigit(char ch) { + return ch >= '0' && ch <= '9'; + } + } +} diff --git a/app/common/src/main/java/stirling/software/common/util/ChecksumUtils.java b/app/common/src/main/java/stirling/software/common/util/ChecksumUtils.java new file mode 100644 index 000000000..d9749deea --- /dev/null +++ b/app/common/src/main/java/stirling/software/common/util/ChecksumUtils.java @@ -0,0 +1,301 @@ +package stirling.software.common.util; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; +import java.util.zip.Adler32; +import java.util.zip.CRC32; +import java.util.zip.Checksum; + +import lombok.experimental.UtilityClass; + +@UtilityClass +public class ChecksumUtils { + + /** Shared buffer size for streaming I/O. */ + private static final int BUFFER_SIZE = 8192; + + /** Mask to extract the lower 32 bits of a long value (unsigned int). */ + private static final long UNSIGNED_32_BIT_MASK = 0xFFFFFFFFL; + + /** + * Computes a checksum for the given file using the chosen algorithm and returns a lowercase hex + * string. + * + *

For digest algorithms (e.g., SHA-256, SHA-1, MD5), this returns the digest as hex. For + * 32-bit {@link Checksum} algorithms ("CRC32", "ADLER32"), this returns an 8-character + * lowercase hex string of the unsigned 32-bit value. + * + * @param path file to read + * @param algorithm algorithm name (case-insensitive). Special values: "CRC32", "ADLER32". + * @return hex string of the checksum + * @throws IOException if the file cannot be read + */ + public static String checksum(Path path, String algorithm) throws IOException { + try (InputStream is = Files.newInputStream(path)) { + return checksum(is, algorithm); + } + } + + /** + * Computes a checksum for the given stream using the chosen algorithm and returns a lowercase + * hex string. + * + *

Note: This method does not close the provided stream. + * + * @param is input stream (not closed by this method) + * @param algorithm algorithm name (case-insensitive). Special values: "CRC32", "ADLER32". + * @return hex string of the checksum + * @throws IOException if reading from the stream fails + */ + public static String checksum(InputStream is, String algorithm) throws IOException { + switch (algorithm.toUpperCase(Locale.ROOT)) { + case "CRC32": + return checksumChecksum(is, new CRC32()); + case "ADLER32": + return checksumChecksum(is, new Adler32()); + default: + return toHex(checksumBytes(is, algorithm)); + } + } + + /** + * Computes a checksum for the given file using the chosen algorithm and returns a Base64 + * encoded string. + * + *

For digest algorithms this is the Base64 of the raw digest bytes. For 32-bit checksum + * algorithms ("CRC32", "ADLER32"), this is the Base64 of the 4-byte big-endian unsigned value. + * + * @param path file to read + * @param algorithm algorithm name (case-insensitive). Special values: "CRC32", "ADLER32". + * @return Base64-encoded checksum bytes + * @throws IOException if the file cannot be read + */ + public static String checksumBase64(Path path, String algorithm) throws IOException { + try (InputStream is = Files.newInputStream(path)) { + return checksumBase64(is, algorithm); + } + } + + /** + * Computes a checksum for the given stream using the chosen algorithm and returns a Base64 + * encoded string. + * + *

Note: This method does not close the provided stream. + * + * @param is input stream (not closed by this method) + * @param algorithm algorithm name (case-insensitive). Special values: "CRC32", "ADLER32". + * @return Base64-encoded checksum bytes + * @throws IOException if reading from the stream fails + */ + public static String checksumBase64(InputStream is, String algorithm) throws IOException { + switch (algorithm.toUpperCase(Locale.ROOT)) { + case "CRC32": + return Base64.getEncoder().encodeToString(checksumChecksumBytes(is, new CRC32())); + case "ADLER32": + return Base64.getEncoder().encodeToString(checksumChecksumBytes(is, new Adler32())); + default: + return Base64.getEncoder().encodeToString(checksumBytes(is, algorithm)); + } + } + + /** + * Computes multiple checksums for the given file in a single pass over the data. + * + *

Returns a map from algorithm name to lowercase hex string. Order of results follows the + * order of the provided {@code algorithms}. + * + * @param path file to read + * @param algorithms algorithm names (case-insensitive). Special: "CRC32", "ADLER32". + * @return map of algorithm → hex string + * @throws IOException if the file cannot be read + */ + public static Map checksums(Path path, String... algorithms) + throws IOException { + try (InputStream is = Files.newInputStream(path)) { + return checksums(is, algorithms); + } + } + + /** + * Computes multiple checksums for the given stream in a single pass over the data. + * + *

Note: This method does not close the provided stream. + * + * @param is input stream (not closed by this method) + * @param algorithms algorithm names (case-insensitive). Special: "CRC32", "ADLER32". + * @return map of algorithm → hex string + * @throws IOException if reading from the stream fails + */ + public static Map checksums(InputStream is, String... algorithms) + throws IOException { + // Use LinkedHashMap to preserve the order of requested algorithms in the result. + Map digests = new LinkedHashMap<>(); + Map checksums = new LinkedHashMap<>(); + + for (String algorithm : algorithms) { + String key = algorithm; // keep original key for output + switch (algorithm.toUpperCase(Locale.ROOT)) { + case "CRC32": + checksums.put(key, new CRC32()); + break; + case "ADLER32": + checksums.put(key, new Adler32()); + break; + default: + try { + // For MessageDigest, pass the original name (case-insensitive per JCA) + digests.put(key, MessageDigest.getInstance(algorithm)); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("Unsupported algorithm: " + algorithm, e); + } + } + } + + byte[] buffer = new byte[BUFFER_SIZE]; + int read; + while ((read = is.read(buffer)) != -1) { + for (MessageDigest digest : digests.values()) { + digest.update(buffer, 0, read); + } + for (Checksum cs : checksums.values()) { + cs.update(buffer, 0, read); + } + } + + Map results = new LinkedHashMap<>(); + for (Map.Entry entry : digests.entrySet()) { + results.put(entry.getKey(), toHex(entry.getValue().digest())); + } + for (Map.Entry entry : checksums.entrySet()) { + // Keep value as long and mask to ensure unsigned hex formatting. + long unsigned32 = entry.getValue().getValue() & UNSIGNED_32_BIT_MASK; + results.put(entry.getKey(), String.format("%08x", unsigned32)); + } + return results; + } + + /** + * Compares the checksum of a file with an expected hex string (case-insensitive). + * + * @param path file to read + * @param algorithm algorithm name (case-insensitive). Special: "CRC32", "ADLER32". + * @param expected expected hex string (case-insensitive) + * @return {@code true} if they match, otherwise {@code false} + * @throws IOException if the file cannot be read + */ + public static boolean matches(Path path, String algorithm, String expected) throws IOException { + try (InputStream is = Files.newInputStream(path)) { + return matches(is, algorithm, expected); + } + } + + /** + * Compares the checksum of a stream with an expected hex string (case-insensitive). + * + *

Note: This method does not close the provided stream. + * + * @param is input stream (not closed by this method) + * @param algorithm algorithm name (case-insensitive). Special: "CRC32", "ADLER32". + * @param expected expected hex string (case-insensitive) + * @return {@code true} if they match, otherwise {@code false} + * @throws IOException if reading from the stream fails + */ + public static boolean matches(InputStream is, String algorithm, String expected) + throws IOException { + return checksum(is, algorithm).equalsIgnoreCase(expected); + } + + // ---------- Internal helpers ---------- + + /** + * Computes a MessageDigest over a stream and returns the raw digest bytes. + * + * @param is input stream (not closed) + * @param algorithm JCA MessageDigest algorithm (e.g., "SHA-256") + * @return raw digest bytes + * @throws IOException if reading fails + * @throws IllegalStateException if the algorithm is unsupported + */ + private static byte[] checksumBytes(InputStream is, String algorithm) throws IOException { + try { + MessageDigest digest = MessageDigest.getInstance(algorithm); + byte[] buffer = new byte[BUFFER_SIZE]; + int read; + while ((read = is.read(buffer)) != -1) { + digest.update(buffer, 0, read); + } + return digest.digest(); + } catch (NoSuchAlgorithmException e) { + // Keep the message explicit to aid debugging + throw new IllegalStateException("Unsupported algorithm: " + algorithm, e); + } + } + + /** + * Computes a 32-bit {@link Checksum} over a stream and returns the lowercase 8-char hex of the + * unsigned 32-bit value. + * + * @param is input stream (not closed) + * @param checksum checksum implementation (CRC32, Adler32, etc.) + * @return 8-character lowercase hex (big-endian representation) + * @throws IOException if reading fails + */ + private static String checksumChecksum(InputStream is, Checksum checksum) throws IOException { + byte[] buffer = new byte[BUFFER_SIZE]; + int read; + while ((read = is.read(buffer)) != -1) { + checksum.update(buffer, 0, read); + } + // Keep as long and mask to ensure correct unsigned representation. + long unsigned32 = checksum.getValue() & UNSIGNED_32_BIT_MASK; + return String.format("%08x", unsigned32); + } + + /** + * Computes a 32-bit {@link Checksum} over a stream and returns the raw 4-byte big-endian + * representation of the unsigned 32-bit value. + * + *

Cast to int already truncates to the lower 32 bits; the sign is irrelevant because we + * serialize the bit pattern directly into 4 bytes. + * + * @param is input stream (not closed) + * @param checksum checksum implementation (CRC32, Adler32, etc.) + * @return 4 bytes (big-endian) + * @throws IOException if reading fails + */ + private static byte[] checksumChecksumBytes(InputStream is, Checksum checksum) + throws IOException { + byte[] buffer = new byte[BUFFER_SIZE]; + int read; + while ((read = is.read(buffer)) != -1) { + checksum.update(buffer, 0, read); + } + // Cast keeps only the lower 32 bits; mask is unnecessary here. + int v = (int) checksum.getValue(); + return ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(v).array(); + } + + /** + * Converts bytes to a lowercase hex string. + * + * @param hash the byte array to convert + * @return the lowercase hex string + */ + private static String toHex(byte[] hash) { + StringBuilder sb = new StringBuilder(hash.length * 2); + for (byte b : hash) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } +} diff --git a/app/common/src/main/java/stirling/software/common/util/CustomHtmlSanitizer.java b/app/common/src/main/java/stirling/software/common/util/CustomHtmlSanitizer.java index 05d9b73a6..c5fb07645 100644 --- a/app/common/src/main/java/stirling/software/common/util/CustomHtmlSanitizer.java +++ b/app/common/src/main/java/stirling/software/common/util/CustomHtmlSanitizer.java @@ -4,7 +4,6 @@ import org.owasp.html.AttributePolicy; import org.owasp.html.HtmlPolicyBuilder; import org.owasp.html.PolicyFactory; import org.owasp.html.Sanitizers; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import stirling.software.common.model.ApplicationProperties; @@ -16,7 +15,6 @@ public class CustomHtmlSanitizer { private final SsrfProtectionService ssrfProtectionService; private final ApplicationProperties applicationProperties; - @Autowired public CustomHtmlSanitizer( SsrfProtectionService ssrfProtectionService, ApplicationProperties applicationProperties) { @@ -28,7 +26,7 @@ public class CustomHtmlSanitizer { new AttributePolicy() { @Override public String apply(String elementName, String attributeName, String value) { - if (value == null || value.trim().isEmpty()) { + if (value.trim().isEmpty()) { return null; } diff --git a/app/common/src/main/java/stirling/software/common/util/EmlParser.java b/app/common/src/main/java/stirling/software/common/util/EmlParser.java index 0815b1c56..ec71fbb19 100644 --- a/app/common/src/main/java/stirling/software/common/util/EmlParser.java +++ b/app/common/src/main/java/stirling/software/common/util/EmlParser.java @@ -6,12 +6,16 @@ import java.io.InputStream; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.nio.charset.StandardCharsets; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Properties; import java.util.regex.Pattern; +import org.springframework.http.MediaType; + import lombok.Data; import lombok.experimental.UtilityClass; @@ -25,11 +29,11 @@ public class EmlParser { private static volatile boolean mimeUtilityChecked = false; private static final Pattern MIME_ENCODED_PATTERN = - Pattern.compile("=\\?([^?]+)\\?([BbQq])\\?([^?]*)\\?="); + RegexPatternUtils.getInstance().getMimeEncodedWordPattern(); private static final String DISPOSITION_ATTACHMENT = "attachment"; - private static final String TEXT_PLAIN = "text/plain"; - private static final String TEXT_HTML = "text/html"; + private static final String TEXT_PLAIN = MediaType.TEXT_PLAIN_VALUE; + private static final String TEXT_HTML = MediaType.TEXT_HTML_VALUE; private static final String MULTIPART_PREFIX = "multipart/"; private static final String HEADER_CONTENT_TYPE = "content-type:"; @@ -69,12 +73,12 @@ public class EmlParser { if (isJakartaMailAvailable()) { return extractEmailContentAdvanced(emlBytes, request, customHtmlSanitizer); } else { - return extractEmailContentBasic(emlBytes, request, customHtmlSanitizer); + return extractEmailContentBasic(emlBytes, customHtmlSanitizer); } } private static EmailContent extractEmailContentBasic( - byte[] emlBytes, EmlToPdfRequest request, CustomHtmlSanitizer customHtmlSanitizer) { + byte[] emlBytes, CustomHtmlSanitizer customHtmlSanitizer) { String emlContent = new String(emlBytes, StandardCharsets.UTF_8); EmailContent content = new EmailContent(); @@ -121,7 +125,7 @@ public class EmlParser { return extractFromMimeMessage(message, request, customHtmlSanitizer); } catch (ReflectiveOperationException e) { - return extractEmailContentBasic(emlBytes, request, customHtmlSanitizer); + return extractEmailContentBasic(emlBytes, customHtmlSanitizer); } } @@ -143,7 +147,11 @@ public class EmlParser { extractRecipients(message, messageClass, content); Method getSentDate = messageClass.getMethod("getSentDate"); - content.setDate((Date) getSentDate.invoke(message)); + Date legacyDate = (Date) getSentDate.invoke(message); + if (legacyDate != null) { + content.setDate( + ZonedDateTime.ofInstant(legacyDate.toInstant(), ZoneId.systemDefault())); + } Method getContent = messageClass.getMethod("getContent"); Object messageContent = getContent.invoke(message); @@ -349,7 +357,11 @@ public class EmlParser { for (String contentIdHeader : contentIdHeaders) { if (contentIdHeader != null && !contentIdHeader.trim().isEmpty()) { attachment.setEmbedded(true); - String contentId = contentIdHeader.trim().replaceAll("[<>]", ""); + String contentId = + RegexPatternUtils.getInstance() + .getAngleBracketsPattern() + .matcher(contentIdHeader.trim()) + .replaceAll(""); attachment.setContentId(contentId); break; } @@ -406,7 +418,8 @@ public class EmlParser { private static String extractBasicHeader(String emlContent, String headerName) { try { - String[] lines = emlContent.split("\r?\n"); + String[] lines = + RegexPatternUtils.getInstance().getNewlineSplitPattern().split(emlContent); for (int i = 0; i < lines.length; i++) { String line = lines[i]; if (line.toLowerCase().startsWith(headerName.toLowerCase())) { @@ -477,7 +490,10 @@ public class EmlParser { } private static int findPartEnd(String content, int start) { - String[] lines = content.substring(start).split("\r?\n"); + String[] lines = + RegexPatternUtils.getInstance() + .getNewlineSplitPattern() + .split(content.substring(start)); StringBuilder result = new StringBuilder(); for (String line : lines) { @@ -491,7 +507,8 @@ public class EmlParser { private static List extractAttachmentsBasic(String emlContent) { List attachments = new ArrayList<>(); try { - String[] lines = emlContent.split("\r?\n"); + String[] lines = + RegexPatternUtils.getInstance().getNewlineSplitPattern().split(emlContent); boolean inHeaders = true; String currentContentType = ""; String currentDisposition = ""; @@ -554,7 +571,11 @@ public class EmlParser { if (filenameStarEnd == -1) filenameStarEnd = disposition.length(); String extendedFilename = disposition.substring(filenameStarStart, filenameStarEnd).trim(); - extendedFilename = extendedFilename.replaceAll("^\"|\"$", ""); + extendedFilename = + RegexPatternUtils.getInstance() + .getQuotesRemovalPattern() + .matcher(extendedFilename) + .replaceAll(""); if (extendedFilename.contains("'")) { String[] parts = extendedFilename.split("'", 3); @@ -569,7 +590,11 @@ public class EmlParser { int filenameEnd = disposition.indexOf(";", filenameStart); if (filenameEnd == -1) filenameEnd = disposition.length(); String filename = disposition.substring(filenameStart, filenameEnd).trim(); - filename = filename.replaceAll("^\"|\"$", ""); + filename = + RegexPatternUtils.getInstance() + .getQuotesRemovalPattern() + .matcher(filename) + .replaceAll(""); return safeMimeDecode(filename); } @@ -614,7 +639,7 @@ public class EmlParser { private String to; private String cc; private String bcc; - private Date date; + private ZonedDateTime date; private String dateString; // For basic parsing fallback private String htmlBody; private String textBody; @@ -622,11 +647,23 @@ public class EmlParser { private List attachments = new ArrayList<>(); public void setHtmlBody(String htmlBody) { - this.htmlBody = htmlBody != null ? htmlBody.replaceAll("\r", "") : null; + this.htmlBody = + htmlBody != null + ? RegexPatternUtils.getInstance() + .getCarriageReturnPattern() + .matcher(htmlBody) + .replaceAll("") + : null; } public void setTextBody(String textBody) { - this.textBody = textBody != null ? textBody.replaceAll("\r", "") : null; + this.textBody = + textBody != null + ? RegexPatternUtils.getInstance() + .getCarriageReturnPattern() + .matcher(textBody) + .replaceAll("") + : null; } } diff --git a/app/common/src/main/java/stirling/software/common/util/EmlProcessingUtils.java b/app/common/src/main/java/stirling/software/common/util/EmlProcessingUtils.java index 9acc30c16..55035fe9f 100644 --- a/app/common/src/main/java/stirling/software/common/util/EmlProcessingUtils.java +++ b/app/common/src/main/java/stirling/software/common/util/EmlProcessingUtils.java @@ -8,6 +8,8 @@ import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.springframework.http.MediaType; + import lombok.experimental.UtilityClass; import stirling.software.common.model.api.converters.EmlToPdfRequest; @@ -33,10 +35,10 @@ public class EmlProcessingUtils { // MIME type detection private static final Map EXTENSION_TO_MIME_TYPE = Map.of( - ".png", "image/png", - ".jpg", "image/jpeg", - ".jpeg", "image/jpeg", - ".gif", "image/gif", + ".png", MediaType.IMAGE_PNG_VALUE, + ".jpg", MediaType.IMAGE_JPEG_VALUE, + ".jpeg", MediaType.IMAGE_JPEG_VALUE, + ".gif", MediaType.IMAGE_GIF_VALUE, ".bmp", "image/bmp", ".webp", "image/webp", ".svg", "image/svg+xml", @@ -81,8 +83,8 @@ public class EmlProcessingUtils { || lowerContent.contains("bcc:"); boolean hasMimeStructure = lowerContent.contains("multipart/") - || lowerContent.contains("text/plain") - || lowerContent.contains("text/html") + || lowerContent.contains(MediaType.TEXT_PLAIN_VALUE) + || lowerContent.contains(MediaType.TEXT_HTML_VALUE) || lowerContent.contains("boundary="); int headerCount = 0; @@ -197,8 +199,16 @@ public class EmlProcessingUtils { String processed = customHtmlSanitizer != null ? customHtmlSanitizer.sanitize(htmlBody) : htmlBody; - processed = processed.replaceAll("(?i)\\s*position\\s*:\\s*fixed[^;]*;?", ""); - processed = processed.replaceAll("(?i)\\s*position\\s*:\\s*absolute[^;]*;?", ""); + processed = + RegexPatternUtils.getInstance() + .getFixedPositionCssPattern() + .matcher(processed) + .replaceAll(""); + processed = + RegexPatternUtils.getInstance() + .getAbsolutePositionCssPattern() + .matcher(processed) + .replaceAll(""); if (emailContent != null && !emailContent.getAttachments().isEmpty()) { processed = PdfAttachmentHandler.processInlineImages(processed, emailContent); @@ -220,14 +230,18 @@ public class EmlProcessingUtils { html = html.replace("\n", "
\n"); html = - html.replaceAll( - "(https?://[\\w\\-._~:/?#\\[\\]@!$&'()*+,;=%]+)", - "$1"); + RegexPatternUtils.getInstance() + .getUrlLinkPattern() + .matcher(html) + .replaceAll( + "$1"); html = - html.replaceAll( - "([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,63})", - "$1"); + RegexPatternUtils.getInstance() + .getEmailLinkPattern() + .matcher(html) + .replaceAll( + "$1"); return html; } @@ -464,7 +478,7 @@ public class EmlProcessingUtils { } } - return "image/png"; + return MediaType.IMAGE_PNG_VALUE; // Default MIME type } public static String decodeUrlEncoded(String encoded) { @@ -488,9 +502,13 @@ public class EmlProcessingUtils { Matcher concatenatedMatcher = concatenatedPattern.matcher(encodedText); String processedText = concatenatedMatcher.replaceAll( - match -> match.group().replaceAll("\\s+(?==\\?)", "")); + match -> + RegexPatternUtils.getInstance() + .getMimeHeaderWhitespacePattern() + .matcher(match.group()) + .replaceAll("")); - Pattern mimePattern = Pattern.compile("=\\?([^?]+)\\?([BbQq])\\?([^?]*)\\?="); + Pattern mimePattern = RegexPatternUtils.getInstance().getMimeEncodedWordPattern(); Matcher matcher = mimePattern.matcher(processedText); int lastEnd = 0; @@ -505,7 +523,11 @@ public class EmlProcessingUtils { String decodedValue = switch (encoding) { case "B" -> { - String cleanBase64 = encodedValue.replaceAll("\\s", ""); + String cleanBase64 = + RegexPatternUtils.getInstance() + .getWhitespacePattern() + .matcher(encodedValue) + .replaceAll(""); byte[] decodedBytes = Base64.getDecoder().decode(cleanBase64); Charset targetCharset; try { @@ -594,8 +616,16 @@ public class EmlProcessingUtils { } public static String simplifyHtmlContent(String htmlContent) { - String simplified = htmlContent.replaceAll("(?i)]*>.*?", ""); - simplified = simplified.replaceAll("(?i)]*>.*?", ""); + String simplified = + RegexPatternUtils.getInstance() + .getScriptTagPattern() + .matcher(htmlContent) + .replaceAll(""); + simplified = + RegexPatternUtils.getInstance() + .getStyleTagPattern() + .matcher(simplified) + .replaceAll(""); return simplified; } } diff --git a/app/common/src/main/java/stirling/software/common/util/ExceptionUtils.java b/app/common/src/main/java/stirling/software/common/util/ExceptionUtils.java index 74f65e713..9f795bb46 100644 --- a/app/common/src/main/java/stirling/software/common/util/ExceptionUtils.java +++ b/app/common/src/main/java/stirling/software/common/util/ExceptionUtils.java @@ -324,4 +324,63 @@ public class ExceptionUtils { return createIllegalArgumentException( "error.argumentRequired", "{0} must not be null", argumentName); } + + /** + * Create a RuntimeException for memory/image size errors when rendering PDF images with DPI. + * Handles OutOfMemoryError and related conditions (e.g., NegativeArraySizeException) that + * result from images exceeding Java's array/memory limits. + * + * @param pageNumber the page number that caused the error + * @param dpi the DPI value used + * @param cause the original error/exception (e.g., OutOfMemoryError, + * NegativeArraySizeException) + * @return RuntimeException with user-friendly message + */ + public static RuntimeException createOutOfMemoryDpiException( + int pageNumber, int dpi, Throwable cause) { + String message = + MessageFormat.format( + "Out of memory or image-too-large error while rendering PDF page {0} at {1} DPI. " + + "This can occur when the resulting image exceeds Java's array/memory limits (e.g., NegativeArraySizeException). " + + "Please use a lower DPI value (recommended: 150 or less) or process the document in smaller chunks.", + pageNumber, dpi); + return new RuntimeException(message, cause); + } + + /** + * Create a RuntimeException for OutOfMemoryError when rendering PDF images with DPI. + * + * @param pageNumber the page number that caused the error + * @param dpi the DPI value used + * @param cause the original OutOfMemoryError + * @return RuntimeException with user-friendly message + */ + public static RuntimeException createOutOfMemoryDpiException( + int pageNumber, int dpi, OutOfMemoryError cause) { + return createOutOfMemoryDpiException(pageNumber, dpi, (Throwable) cause); + } + + /** + * Create a RuntimeException for memory/image size errors when rendering PDF images with DPI. + * Handles OutOfMemoryError and related conditions (e.g., NegativeArraySizeException) that + * result from images exceeding Java's array/memory limits. + * + * @param dpi the DPI value used + * @param cause the original error/exception (e.g., OutOfMemoryError, + * NegativeArraySizeException) + * @return RuntimeException with user-friendly message + */ + public static RuntimeException createOutOfMemoryDpiException(int dpi, Throwable cause) { + String message = + MessageFormat.format( + "Out of memory or image-too-large error while rendering PDF at {0} DPI. " + + "This can occur when the resulting image exceeds Java's array/memory limits (e.g., NegativeArraySizeException). " + + "Please use a lower DPI value (recommended: 150 or less) or process the document in smaller chunks.", + dpi); + return new RuntimeException(message, cause); + } + + public static RuntimeException createOutOfMemoryDpiException(int dpi, OutOfMemoryError cause) { + return createOutOfMemoryDpiException(dpi, (Throwable) cause); + } } diff --git a/app/common/src/main/java/stirling/software/common/util/FileToPdf.java b/app/common/src/main/java/stirling/software/common/util/FileToPdf.java index 799f91e05..d02236ce2 100644 --- a/app/common/src/main/java/stirling/software/common/util/FileToPdf.java +++ b/app/common/src/main/java/stirling/software/common/util/FileToPdf.java @@ -1,6 +1,9 @@ package stirling.software.common.util; -import java.io.*; +import java.io.ByteArrayInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; import java.nio.file.FileVisitResult; import java.nio.file.Files; @@ -124,20 +127,21 @@ public class FileToPdf { private static void zipDirectory(Path sourceDir, Path zipFilePath) throws IOException { try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFilePath.toFile()))) { - Files.walk(sourceDir) - .filter(path -> !Files.isDirectory(path)) - .forEach( - path -> { - ZipEntry zipEntry = - new ZipEntry(sourceDir.relativize(path).toString()); - try { - zos.putNextEntry(zipEntry); - Files.copy(path, zos); - zos.closeEntry(); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - }); + try (Stream walk = Files.walk(sourceDir)) { + walk.filter(path -> !Files.isDirectory(path)) + .forEach( + path -> { + ZipEntry zipEntry = + new ZipEntry(sourceDir.relativize(path).toString()); + try { + zos.putNextEntry(zipEntry); + Files.copy(path, zos); + zos.closeEntry(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + } } } @@ -204,15 +208,27 @@ public class FileToPdf { return ""; } // Remove any drive letters (e.g., "C:\") and leading forward/backslashes - entryName = entryName.replaceAll("^[a-zA-Z]:[\\\\/]+", ""); - entryName = entryName.replaceAll("^[\\\\/]+", ""); + entryName = + RegexPatternUtils.getInstance() + .getDriveLetterPattern() + .matcher(entryName) + .replaceAll(""); + entryName = + RegexPatternUtils.getInstance() + .getLeadingSlashesPattern() + .matcher(entryName) + .replaceAll(""); // Recursively remove path traversal sequences while (entryName.contains("../") || entryName.contains("..\\")) { entryName = entryName.replace("../", "").replace("..\\", ""); } // Normalize all backslashes to forward slashes - entryName = entryName.replaceAll("\\\\", "/"); + entryName = + RegexPatternUtils.getInstance() + .getBackslashPattern() + .matcher(entryName) + .replaceAll("/"); return entryName; } } diff --git a/app/common/src/main/java/stirling/software/common/util/FormUtils.java b/app/common/src/main/java/stirling/software/common/util/FormUtils.java new file mode 100644 index 000000000..19cda95ed --- /dev/null +++ b/app/common/src/main/java/stirling/software/common/util/FormUtils.java @@ -0,0 +1,658 @@ +package stirling.software.common.util; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.pdfbox.cos.COSName; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDDocumentCatalog; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDResources; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.font.PDType1Font; +import org.apache.pdfbox.pdmodel.font.Standard14Fonts; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget; +import org.apache.pdfbox.pdmodel.interactive.form.*; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public final class FormUtils { + + private FormUtils() {} + + public static boolean hasAnyRotatedPage(PDDocument document) { + try { + for (PDPage page : document.getPages()) { + int rot = page.getRotation(); + int norm = ((rot % 360) + 360) % 360; + if (norm != 0) { + return true; + } + } + } catch (Exception e) { + log.warn("Failed to inspect page rotations: {}", e.getMessage(), e); + } + return false; + } + + public static void copyAndTransformFormFields( + PDDocument sourceDocument, + PDDocument newDocument, + int totalPages, + int pagesPerSheet, + int cols, + int rows, + float cellWidth, + float cellHeight) + throws IOException { + + PDDocumentCatalog sourceCatalog = sourceDocument.getDocumentCatalog(); + PDAcroForm sourceAcroForm = sourceCatalog.getAcroForm(); + + if (sourceAcroForm == null || sourceAcroForm.getFields().isEmpty()) { + return; + } + + PDDocumentCatalog newCatalog = newDocument.getDocumentCatalog(); + PDAcroForm newAcroForm = new PDAcroForm(newDocument); + newCatalog.setAcroForm(newAcroForm); + + PDResources dr = new PDResources(); + PDType1Font helvetica = new PDType1Font(Standard14Fonts.FontName.HELVETICA); + PDType1Font zapfDingbats = new PDType1Font(Standard14Fonts.FontName.ZAPF_DINGBATS); + dr.put(COSName.getPDFName("Helv"), helvetica); + dr.put(COSName.getPDFName("ZaDb"), zapfDingbats); + newAcroForm.setDefaultResources(dr); + newAcroForm.setDefaultAppearance("/Helv 12 Tf 0 g"); + + // Do not mutate the source AcroForm; skip bad widgets during copy + newAcroForm.setNeedAppearances(true); + + Map fieldNameCounters = new HashMap<>(); + + // Build widget -> field map once for efficient lookups + Map widgetFieldMap = buildWidgetFieldMap(sourceAcroForm); + + for (int pageIndex = 0; pageIndex < totalPages; pageIndex++) { + PDPage sourcePage = sourceDocument.getPage(pageIndex); + List annotations = sourcePage.getAnnotations(); + + if (annotations.isEmpty()) { + continue; + } + + int destinationPageIndex = pageIndex / pagesPerSheet; + int adjustedPageIndex = pageIndex % pagesPerSheet; + int rowIndex = adjustedPageIndex / cols; + int colIndex = adjustedPageIndex % cols; + + if (destinationPageIndex >= newDocument.getNumberOfPages()) { + continue; + } + + PDPage destinationPage = newDocument.getPage(destinationPageIndex); + PDRectangle sourceRect = sourcePage.getMediaBox(); + + float scaleWidth = cellWidth / sourceRect.getWidth(); + float scaleHeight = cellHeight / sourceRect.getHeight(); + float scale = Math.min(scaleWidth, scaleHeight); + + float x = colIndex * cellWidth + (cellWidth - sourceRect.getWidth() * scale) / 2; + float y = + destinationPage.getMediaBox().getHeight() + - ((rowIndex + 1) * cellHeight + - (cellHeight - sourceRect.getHeight() * scale) / 2); + + copyBasicFormFields( + sourceAcroForm, + newAcroForm, + sourcePage, + destinationPage, + x, + y, + scale, + pageIndex, + fieldNameCounters, + widgetFieldMap); + } + + // Refresh appearances to ensure widgets render correctly across viewers + try { + // Use reflection to avoid compile-time dependency on PDFBox version + Method m = newAcroForm.getClass().getMethod("refreshAppearances"); + m.invoke(newAcroForm); + } catch (NoSuchMethodException nsme) { + log.warn( + "AcroForm.refreshAppearances() not available in this PDFBox version; relying on NeedAppearances."); + } catch (Throwable t) { + log.warn("Failed to refresh field appearances via AcroForm: {}", t.getMessage(), t); + } + } + + private static void copyBasicFormFields( + PDAcroForm sourceAcroForm, + PDAcroForm newAcroForm, + PDPage sourcePage, + PDPage destinationPage, + float offsetX, + float offsetY, + float scale, + int pageIndex, + Map fieldNameCounters, + Map widgetFieldMap) { + + try { + List sourceAnnotations = sourcePage.getAnnotations(); + List destinationAnnotations = destinationPage.getAnnotations(); + + for (PDAnnotation annotation : sourceAnnotations) { + if (annotation instanceof PDAnnotationWidget widgetAnnotation) { + if (widgetAnnotation.getRectangle() == null) { + continue; + } + PDField sourceField = + widgetFieldMap != null ? widgetFieldMap.get(widgetAnnotation) : null; + if (sourceField == null) { + continue; // skip widgets without a matching field + } + if (sourceField instanceof PDTextField pdtextfield) { + createSimpleTextField( + newAcroForm, + destinationPage, + destinationAnnotations, + pdtextfield, + widgetAnnotation, + offsetX, + offsetY, + scale, + pageIndex, + fieldNameCounters); + } else if (sourceField instanceof PDCheckBox pdCheckBox) { + createSimpleCheckBoxField( + newAcroForm, + destinationPage, + destinationAnnotations, + pdCheckBox, + widgetAnnotation, + offsetX, + offsetY, + scale, + pageIndex, + fieldNameCounters); + } else if (sourceField instanceof PDRadioButton pdRadioButton) { + createSimpleRadioButtonField( + newAcroForm, + destinationPage, + destinationAnnotations, + pdRadioButton, + widgetAnnotation, + offsetX, + offsetY, + scale, + pageIndex, + fieldNameCounters); + } else if (sourceField instanceof PDComboBox pdComboBox) { + createSimpleComboBoxField( + newAcroForm, + destinationPage, + destinationAnnotations, + pdComboBox, + widgetAnnotation, + offsetX, + offsetY, + scale, + pageIndex, + fieldNameCounters); + } else if (sourceField instanceof PDListBox pdlistbox) { + createSimpleListBoxField( + newAcroForm, + destinationPage, + destinationAnnotations, + pdlistbox, + widgetAnnotation, + offsetX, + offsetY, + scale, + pageIndex, + fieldNameCounters); + } else if (sourceField instanceof PDSignatureField pdSignatureField) { + createSimpleSignatureField( + newAcroForm, + destinationPage, + destinationAnnotations, + pdSignatureField, + widgetAnnotation, + offsetX, + offsetY, + scale, + pageIndex, + fieldNameCounters); + } else if (sourceField instanceof PDPushButton pdPushButton) { + createSimplePushButtonField( + newAcroForm, + destinationPage, + destinationAnnotations, + pdPushButton, + widgetAnnotation, + offsetX, + offsetY, + scale, + pageIndex, + fieldNameCounters); + } + } + } + } catch (Exception e) { + log.warn( + "Failed to copy basic form fields for page {}: {}", + pageIndex, + e.getMessage(), + e); + } + } + + private static void createSimpleTextField( + PDAcroForm newAcroForm, + PDPage destinationPage, + List destinationAnnotations, + PDTextField sourceField, + PDAnnotationWidget sourceWidget, + float offsetX, + float offsetY, + float scale, + int pageIndex, + Map fieldNameCounters) { + + try { + PDTextField newTextField = new PDTextField(newAcroForm); + newTextField.setDefaultAppearance("/Helv 12 Tf 0 g"); + + boolean initialized = + initializeFieldWithWidget( + newAcroForm, + destinationPage, + destinationAnnotations, + newTextField, + sourceField.getPartialName(), + "textField", + sourceWidget, + offsetX, + offsetY, + scale, + pageIndex, + fieldNameCounters); + + if (!initialized) { + return; + } + + if (sourceField.getValueAsString() != null) { + newTextField.setValue(sourceField.getValueAsString()); + } + + } catch (Exception e) { + log.warn( + "Failed to create text field '{}': {}", + sourceField.getPartialName(), + e.getMessage(), + e); + } + } + + private static void createSimpleCheckBoxField( + PDAcroForm newAcroForm, + PDPage destinationPage, + List destinationAnnotations, + PDCheckBox sourceField, + PDAnnotationWidget sourceWidget, + float offsetX, + float offsetY, + float scale, + int pageIndex, + Map fieldNameCounters) { + + try { + PDCheckBox newCheckBox = new PDCheckBox(newAcroForm); + + boolean initialized = + initializeFieldWithWidget( + newAcroForm, + destinationPage, + destinationAnnotations, + newCheckBox, + sourceField.getPartialName(), + "checkBox", + sourceWidget, + offsetX, + offsetY, + scale, + pageIndex, + fieldNameCounters); + + if (!initialized) { + return; + } + + if (sourceField.isChecked()) { + newCheckBox.check(); + } else { + newCheckBox.unCheck(); + } + + } catch (Exception e) { + log.warn( + "Failed to create checkbox field '{}': {}", + sourceField.getPartialName(), + e.getMessage(), + e); + } + } + + private static void createSimpleRadioButtonField( + PDAcroForm newAcroForm, + PDPage destinationPage, + List destinationAnnotations, + PDRadioButton sourceField, + PDAnnotationWidget sourceWidget, + float offsetX, + float offsetY, + float scale, + int pageIndex, + Map fieldNameCounters) { + + try { + PDRadioButton newRadioButton = new PDRadioButton(newAcroForm); + + boolean initialized = + initializeFieldWithWidget( + newAcroForm, + destinationPage, + destinationAnnotations, + newRadioButton, + sourceField.getPartialName(), + "radioButton", + sourceWidget, + offsetX, + offsetY, + scale, + pageIndex, + fieldNameCounters); + + if (!initialized) { + return; + } + + if (sourceField.getExportValues() != null) { + newRadioButton.setExportValues(sourceField.getExportValues()); + } + if (sourceField.getValue() != null) { + newRadioButton.setValue(sourceField.getValue()); + } + } catch (Exception e) { + log.warn( + "Failed to create radio button field '{}': {}", + sourceField.getPartialName(), + e.getMessage(), + e); + } + } + + private static void createSimpleComboBoxField( + PDAcroForm newAcroForm, + PDPage destinationPage, + List destinationAnnotations, + PDComboBox sourceField, + PDAnnotationWidget sourceWidget, + float offsetX, + float offsetY, + float scale, + int pageIndex, + Map fieldNameCounters) { + + try { + PDComboBox newComboBox = new PDComboBox(newAcroForm); + + boolean initialized = + initializeFieldWithWidget( + newAcroForm, + destinationPage, + destinationAnnotations, + newComboBox, + sourceField.getPartialName(), + "comboBox", + sourceWidget, + offsetX, + offsetY, + scale, + pageIndex, + fieldNameCounters); + + if (!initialized) { + return; + } + + if (sourceField.getOptions() != null) { + newComboBox.setOptions(sourceField.getOptions()); + } + if (sourceField.getValue() != null && !sourceField.getValue().isEmpty()) { + newComboBox.setValue(sourceField.getValue()); + } + } catch (Exception e) { + log.warn( + "Failed to create combo box field '{}': {}", + sourceField.getPartialName(), + e.getMessage(), + e); + } + } + + private static void createSimpleListBoxField( + PDAcroForm newAcroForm, + PDPage destinationPage, + List destinationAnnotations, + PDListBox sourceField, + PDAnnotationWidget sourceWidget, + float offsetX, + float offsetY, + float scale, + int pageIndex, + Map fieldNameCounters) { + + try { + PDListBox newListBox = new PDListBox(newAcroForm); + + boolean initialized = + initializeFieldWithWidget( + newAcroForm, + destinationPage, + destinationAnnotations, + newListBox, + sourceField.getPartialName(), + "listBox", + sourceWidget, + offsetX, + offsetY, + scale, + pageIndex, + fieldNameCounters); + + if (!initialized) { + return; + } + + if (sourceField.getOptions() != null) { + newListBox.setOptions(sourceField.getOptions()); + } + if (sourceField.getValue() != null && !sourceField.getValue().isEmpty()) { + newListBox.setValue(sourceField.getValue()); + } + } catch (Exception e) { + log.warn( + "Failed to create list box field '{}': {}", + sourceField.getPartialName(), + e.getMessage(), + e); + } + } + + private static void createSimpleSignatureField( + PDAcroForm newAcroForm, + PDPage destinationPage, + List destinationAnnotations, + PDSignatureField sourceField, + PDAnnotationWidget sourceWidget, + float offsetX, + float offsetY, + float scale, + int pageIndex, + Map fieldNameCounters) { + + try { + PDSignatureField newSignatureField = new PDSignatureField(newAcroForm); + + boolean initialized = + initializeFieldWithWidget( + newAcroForm, + destinationPage, + destinationAnnotations, + newSignatureField, + sourceField.getPartialName(), + "signature", + sourceWidget, + offsetX, + offsetY, + scale, + pageIndex, + fieldNameCounters); + + if (!initialized) { + return; + } + } catch (Exception e) { + log.warn( + "Failed to create signature field '{}': {}", + sourceField.getPartialName(), + e.getMessage(), + e); + } + } + + private static void createSimplePushButtonField( + PDAcroForm newAcroForm, + PDPage destinationPage, + List destinationAnnotations, + PDPushButton sourceField, + PDAnnotationWidget sourceWidget, + float offsetX, + float offsetY, + float scale, + int pageIndex, + Map fieldNameCounters) { + + try { + PDPushButton newPushButton = new PDPushButton(newAcroForm); + + boolean initialized = + initializeFieldWithWidget( + newAcroForm, + destinationPage, + destinationAnnotations, + newPushButton, + sourceField.getPartialName(), + "pushButton", + sourceWidget, + offsetX, + offsetY, + scale, + pageIndex, + fieldNameCounters); + + } catch (Exception e) { + log.warn( + "Failed to create push button field '{}': {}", + sourceField.getPartialName(), + e.getMessage(), + e); + } + } + + private static boolean initializeFieldWithWidget( + PDAcroForm newAcroForm, + PDPage destinationPage, + List destinationAnnotations, + T newField, + String originalName, + String fallbackName, + PDAnnotationWidget sourceWidget, + float offsetX, + float offsetY, + float scale, + int pageIndex, + Map fieldNameCounters) { + + String baseName = (originalName != null) ? originalName : fallbackName; + String newFieldName = generateUniqueFieldName(baseName, pageIndex, fieldNameCounters); + newField.setPartialName(newFieldName); + + PDAnnotationWidget newWidget = new PDAnnotationWidget(); + PDRectangle sourceRect = sourceWidget.getRectangle(); + if (sourceRect == null) { + return false; + } + + float newX = (sourceRect.getLowerLeftX() * scale) + offsetX; + float newY = (sourceRect.getLowerLeftY() * scale) + offsetY; + float newWidth = sourceRect.getWidth() * scale; + float newHeight = sourceRect.getHeight() * scale; + newWidget.setRectangle(new PDRectangle(newX, newY, newWidth, newHeight)); + newWidget.setPage(destinationPage); + + newField.getWidgets().add(newWidget); + newWidget.setParent(newField); + newAcroForm.getFields().add(newField); + destinationAnnotations.add(newWidget); + return true; + } + + private static String generateUniqueFieldName( + String originalName, int pageIndex, Map fieldNameCounters) { + String baseName = "page" + pageIndex + "_" + originalName; + + Integer counter = fieldNameCounters.get(baseName); + if (counter == null) { + counter = 0; + } else { + counter++; + } + fieldNameCounters.put(baseName, counter); + + return counter == 0 ? baseName : baseName + "_" + counter; + } + + private static Map buildWidgetFieldMap(PDAcroForm acroForm) { + Map map = new HashMap<>(); + if (acroForm == null) { + return map; + } + try { + for (PDField field : acroForm.getFieldTree()) { + List widgets = field.getWidgets(); + if (widgets != null) { + for (PDAnnotationWidget w : widgets) { + if (w != null) { + map.put(w, field); + } + } + } + } + } catch (Exception e) { + log.warn("Failed to build widget->field map: {}", e.getMessage(), e); + } + return map; + } +} diff --git a/app/common/src/main/java/stirling/software/common/util/GeneralUtils.java b/app/common/src/main/java/stirling/software/common/util/GeneralUtils.java index 9f8d7a7e0..10ac8b595 100644 --- a/app/common/src/main/java/stirling/software/common/util/GeneralUtils.java +++ b/app/common/src/main/java/stirling/software/common/util/GeneralUtils.java @@ -9,13 +9,9 @@ import java.nio.charset.StandardCharsets; import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributes; import java.security.MessageDigest; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Enumeration; -import java.util.List; -import java.util.Locale; -import java.util.Set; -import java.util.UUID; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; @@ -28,25 +24,37 @@ import com.fathzer.soft.javaluator.DoubleEvaluator; import io.github.pixee.security.HostValidator; import io.github.pixee.security.Urls; +import lombok.experimental.UtilityClass; import lombok.extern.slf4j.Slf4j; import stirling.software.common.configuration.InstallationPathConfig; @Slf4j +@UtilityClass public class GeneralUtils { - private static final Set DEFAULT_VALID_SCRIPTS = - Set.of("png_to_webp.py", "split_photos.py"); - private static final Set DEFAULT_VALID_PIPELINE = + private final Set DEFAULT_VALID_SCRIPTS = Set.of("png_to_webp.py", "split_photos.py"); + private final Set DEFAULT_VALID_PIPELINE = Set.of( "OCR images.json", "Prepare-pdfs-for-email.json", "split-rotate-auto-rename.json"); - private static final String DEFAULT_WEBUI_CONFIGS_DIR = "defaultWebUIConfigs"; - private static final String PYTHON_SCRIPTS_DIR = "python"; + private final String DEFAULT_WEBUI_CONFIGS_DIR = "defaultWebUIConfigs"; + private final String PYTHON_SCRIPTS_DIR = "python"; + private final RegexPatternUtils patternCache = RegexPatternUtils.getInstance(); + // Valid size units used for convertSizeToBytes validation and parsing + private final Set VALID_SIZE_UNITS = Set.of("B", "KB", "MB", "GB", "TB"); - public static File convertMultipartFileToFile(MultipartFile multipartFile) throws IOException { + /* + * Converts a MultipartFile to a regular File with improved performance and security. + * + * @param multipartFile the multipart file to convert + * @return temporary File containing the multipart file data + * @throws IOException if I/O error occurs during conversion + * @throws IllegalArgumentException if file exceeds maximum allowed size + */ + public File convertMultipartFileToFile(MultipartFile multipartFile) throws IOException { String customTempDir = System.getenv("STIRLING_TEMPFILES_DIRECTORY"); if (customTempDir == null || customTempDir.isEmpty()) { customTempDir = System.getProperty("stirling.tempfiles.directory"); @@ -81,10 +89,137 @@ public class GeneralUtils { return tempFile; } - public static void deleteDirectory(Path path) throws IOException { + /* + * Gets the configured temporary directory, creating it if necessary. + * + * @return Path to the temporary directory + * @throws IOException if directory creation fails + */ + private Path getTempDirectory() throws IOException { + String customTempDir = System.getenv("STIRLING_TEMPFILES_DIRECTORY"); + if (customTempDir == null || customTempDir.isEmpty()) { + customTempDir = System.getProperty("stirling.tempfiles.directory"); + } + + Path tempDir; + if (customTempDir != null && !customTempDir.isEmpty()) { + tempDir = Path.of(customTempDir); + } else { + tempDir = Path.of(System.getProperty("java.io.tmpdir"), "stirling-pdf"); + } + + if (!Files.exists(tempDir)) { + Files.createDirectories(tempDir); + } + + return tempDir; + } + + /* + * Remove file extension + * + *

Uses fast string operations for common cases (valid extensions) and falls back to + * optimized regex for edge cases (no extension, hidden files, etc.). + * + *

    + *
  • String operations avoid regex engine overhead for common cases + *
  • Cached pattern compilation eliminates recompilation costs + *
  • Fresh Matcher instances ensure thread safety + *
+ * + * @param filename the filename to process, may be null + * @return filename without extension, or "default" if input is null + */ + public String removeExtension(String filename) { + if (filename == null) { + return "default"; + } + + if (filename.isEmpty()) { + return filename; + } + + int dotIndex = filename.lastIndexOf('.'); + if (dotIndex > 0 && dotIndex < filename.length() - 1) { + return filename.substring(0, dotIndex); + } + + if (dotIndex == 0 || dotIndex == filename.length() - 1 || dotIndex == -1) { + return filename; + } + + Pattern pattern = patternCache.getPattern(RegexPatternUtils.getExtensionRegex()); + Matcher matcher = pattern.matcher(filename); + return matcher.find() ? matcher.replaceFirst("") : filename; + } + + /* + * Append suffix to base name with null safety. + * + * @param baseName the base filename, null becomes "default" + * @param suffix the suffix to append, null becomes empty string + * @return concatenated string with null safety + */ + public String appendSuffix(String baseName, String suffix) { + return (baseName == null ? "default" : baseName) + (suffix != null ? suffix : ""); + } + + /* + * Generate a PDF filename by removing extension from first file and adding suffix. + * + *

High-level utility method for common PDF naming scenarios. Handles null safety and uses + * extension removal. + * + * @param firstFilename the filename of the first file being, may be null + * @param suffix the suffix to append (e.g., "_merged.pdf") + * @return filename with suffix, or default name if input is null + */ + public String generateFilename(String firstFilename, String suffix) { + String baseName = removeExtension(firstFilename); + return appendSuffix(baseName, suffix); + } + + /* + * Process a list of filenames by removing extensions and adding suffix. + * + *

Efficiently processes multiple filenames using streaming operations and bulk operations + * where possible. Handles null safety for both input list and individual filenames. + * + * @param filenames the list of filenames to process, may be null + * @param suffix the suffix to append to each processed filename + * @param processor consumer to handle each processed filename, may be null + */ + public void processFilenames( + List filenames, String suffix, java.util.function.Consumer processor) { + if (filenames == null || processor == null) { + return; + } + + filenames.stream() + .map(filename -> appendSuffix(removeExtension(filename), suffix)) + .forEach(processor); + } + + /* + * Extract title from filename by removing extension, with fallback handling. + * + *

Returns "Untitled" for null or empty filenames, otherwise removes the extension using the + * optimized removeExtension method. + * + * @param filename the filename to extract title from, may be null + * @return the title without extension, or "Untitled" if input is null/empty + */ + public String getTitleFromFilename(String filename) { + if (filename == null || filename.isEmpty()) { + return "Untitled"; + } + return removeExtension(filename); + } + + public void deleteDirectory(Path path) throws IOException { Files.walkFileTree( path, - new SimpleFileVisitor() { + new SimpleFileVisitor<>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { @@ -101,8 +236,18 @@ public class GeneralUtils { }); } - public static String convertToFileName(String name) { - String safeName = name.replaceAll("[^a-zA-Z0-9]", "_"); + public String convertToFileName(String name) { + if (name == null) return "_"; + StringBuilder safeNameBuilder = new StringBuilder(name.length()); + for (int i = 0; i < name.length(); i++) { + char c = name.charAt(i); + if (Character.isLetterOrDigit(c)) { + safeNameBuilder.append(c); + } else { + safeNameBuilder.append('_'); + } + } + String safeName = safeNameBuilder.toString(); if (safeName.length() > 50) { safeName = safeName.substring(0, 50); } @@ -110,19 +255,20 @@ public class GeneralUtils { } // Get resources from a location pattern - public static Resource[] getResourcesFromLocationPattern( + public Resource[] getResourcesFromLocationPattern( String locationPattern, ResourceLoader resourceLoader) throws Exception { // Normalize the path for file resources - if (locationPattern.startsWith("file:")) { - String rawPath = locationPattern.substring(5).replace("\\*", "").replace("/*", ""); + String pattern = locationPattern; + if (pattern.startsWith("file:")) { + String rawPath = pattern.substring(5).replace("\\*", "").replace("/*", ""); Path normalizePath = Paths.get(rawPath).normalize(); - locationPattern = "file:" + normalizePath.toString().replace("\\", "/") + "/*"; + pattern = "file:" + normalizePath.toString().replace("\\", "/") + "/*"; } return ResourcePatternUtils.getResourcePatternResolver(resourceLoader) - .getResources(locationPattern); + .getResources(pattern); } - public static boolean isValidURL(String urlStr) { + public boolean isValidURL(String urlStr) { try { Urls.create( urlStr, Urls.HTTP_PROTOCOLS, HostValidator.DENY_COMMON_INFRASTRUCTURE_TARGETS); @@ -132,7 +278,25 @@ public class GeneralUtils { } } - public static boolean isURLReachable(String urlStr) { + /* + * Checks if a URL is reachable with proper timeout configuration and error handling. + * + * @param urlStr the URL string to check + * @return true if URL is reachable, false otherwise + */ + public boolean isURLReachable(String urlStr) { + return isURLReachable(urlStr, 5000, 5000); + } + + /* + * Checks if a URL is reachable with configurable timeouts. + * + * @param urlStr the URL string to check + * @param connectTimeout connection timeout in milliseconds + * @param readTimeout read timeout in milliseconds + * @return true if URL is reachable, false otherwise + */ + public boolean isURLReachable(String urlStr, int connectTimeout, int readTimeout) { try { // Parse the URL URL url = URI.create(urlStr).toURL(); @@ -152,16 +316,19 @@ public class GeneralUtils { // Check if the URL is reachable HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("HEAD"); - // connection.setConnectTimeout(5000); // Set connection timeout - // connection.setReadTimeout(5000); // Set read timeout + connection.setConnectTimeout(connectTimeout); + connection.setReadTimeout(readTimeout); + connection.setInstanceFollowRedirects(false); // Security: prevent redirect loops + int responseCode = connection.getResponseCode(); return (200 <= responseCode && responseCode <= 399); } catch (Exception e) { + log.debug("URL {} is not reachable: {}", urlStr, e.getMessage()); return false; // Return false in case of any exception } } - private static boolean isLocalAddress(String host) { + private boolean isLocalAddress(String host) { try { // Resolve DNS to IP address InetAddress address = InetAddress.getByName(host); @@ -181,7 +348,14 @@ public class GeneralUtils { } } - public static File multipartToFile(MultipartFile multipart) throws IOException { + /* + * Improved multipart file conversion using the shared helper method. + * + * @param multipart the multipart file to convert + * @return temporary File containing the multipart file data + * @throws IOException if I/O error occurs during conversion + */ + public File multipartToFile(MultipartFile multipart) throws IOException { Path tempFile = Files.createTempFile("overlay-", ".pdf"); try (InputStream in = multipart.getInputStream(); FileOutputStream out = new FileOutputStream(tempFile.toFile())) { @@ -194,54 +368,105 @@ public class GeneralUtils { return tempFile.toFile(); } - public static Long convertSizeToBytes(String sizeStr) { + /* + * Supports TB/PB units and provides detailed error messages. + * + * @param sizeStr the size string to convert (e.g., "100MB", "1.5GB") + * @param defaultUnit the default unit to assume if none specified ("MB", "GB", etc.) + * @return size in bytes, or null if parsing fails + * @throws IllegalArgumentException if defaultUnit is invalid + */ + public Long convertSizeToBytes(String sizeStr, String defaultUnit) { if (sizeStr == null) { return null; } + if (defaultUnit != null && !isValidSizeUnit(defaultUnit)) { + throw new IllegalArgumentException("Invalid default unit: " + defaultUnit); + } + sizeStr = sizeStr.trim().toUpperCase(); sizeStr = sizeStr.replace(",", ".").replace(" ", ""); + try { - if (sizeStr.endsWith("KB")) { - return (long) - (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) * 1024); - } else if (sizeStr.endsWith("MB")) { + if (sizeStr.endsWith("TB")) { return (long) (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) - * 1024 - * 1024); + * 1024L + * 1024L + * 1024L + * 1024L); } else if (sizeStr.endsWith("GB")) { return (long) (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) - * 1024 - * 1024 - * 1024); - } else if (sizeStr.endsWith("B")) { + * 1024L + * 1024L + * 1024L); + } else if (sizeStr.endsWith("MB")) { + return (long) + (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) + * 1024L + * 1024L); + } else if (sizeStr.endsWith("KB")) { + return (long) + (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) * 1024L); + } else if (!sizeStr.isEmpty() && sizeStr.charAt(sizeStr.length() - 1) == 'B') { return Long.parseLong(sizeStr.substring(0, sizeStr.length() - 1)); } else { - // Assume MB if no unit is specified - return (long) (Double.parseDouble(sizeStr) * 1024 * 1024); + // Use provided default unit or fall back to MB + String unit = defaultUnit != null ? defaultUnit.toUpperCase() : "MB"; + double value = Double.parseDouble(sizeStr); + return switch (unit) { + case "TB" -> (long) (value * 1024L * 1024L * 1024L * 1024L); + case "GB" -> (long) (value * 1024L * 1024L * 1024L); + case "MB" -> (long) (value * 1024L * 1024L); + case "KB" -> (long) (value * 1024L); + case "B" -> (long) value; + default -> (long) (value * 1024L * 1024L); // Default to MB + }; } } catch (NumberFormatException e) { - // The numeric part of the input string cannot be parsed, handle this case + log.warn("Failed to parse size string '{}': {}", sizeStr, e.getMessage()); + return null; } - - return null; } - public static String formatBytes(long bytes) { + /* + * Converts size string to bytes using MB as default unit. + * + * @param sizeStr the size string to convert + * @return size in bytes, or null if parsing fails + */ + public Long convertSizeToBytes(String sizeStr) { + return convertSizeToBytes(sizeStr, "MB"); + } + + /* Validates if a string represents a valid size unit. */ + private boolean isValidSizeUnit(String unit) { + // Use a precomputed Set for O(1) lookup, normalize using a locale-safe toUpperCase + return unit != null && VALID_SIZE_UNITS.contains(unit.toUpperCase(Locale.ROOT)); + } + + /* Enhanced byte formatting with TB/PB support and better precision. */ + public String formatBytes(long bytes) { + if (bytes < 0) { + return "Invalid size"; + } + if (bytes < 1024) { return bytes + " B"; - } else if (bytes < 1024 * 1024) { + } else if (bytes < 1024L * 1024L) { return String.format(Locale.US, "%.2f KB", bytes / 1024.0); - } else if (bytes < 1024 * 1024 * 1024) { + } else if (bytes < 1024L * 1024L * 1024L) { return String.format(Locale.US, "%.2f MB", bytes / (1024.0 * 1024.0)); - } else { + } else if (bytes < 1024L * 1024L * 1024L * 1024L) { return String.format(Locale.US, "%.2f GB", bytes / (1024.0 * 1024.0 * 1024.0)); + } else { + return String.format(Locale.US, "%.2f TB", bytes / (1024.0 * 1024.0 * 1024.0 * 1024.0)); } } - public static List parsePageList(String pages, int totalPages, boolean oneBased) { + public List parsePageList(String pages, int totalPages, boolean oneBased) { if (pages == null) { return List.of(1); // Default to first page if input is null } @@ -252,11 +477,11 @@ public class GeneralUtils { } } - public static List parsePageList(String[] pages, int totalPages) { + public List parsePageList(String[] pages, int totalPages) { return parsePageList(pages, totalPages, false); } - public static List parsePageList(String[] pages, int totalPages, boolean oneBased) { + public List parsePageList(String[] pages, int totalPages, boolean oneBased) { List result = new ArrayList<>(); int offset = oneBased ? 1 : 0; for (String page : pages) { @@ -278,33 +503,72 @@ public class GeneralUtils { return result; } - public static List evaluateNFunc(String expression, int maxValue) { + /* + * Enhanced mathematical expression evaluation with bounds checking and timeout protection. + * + * @param expression the mathematical expression containing 'n' + * @param maxValue the maximum value for 'n' and result bounds + * @return list of valid page numbers + * @throws IllegalArgumentException if expression is invalid or unsafe + */ + public List evaluateNFunc(String expression, int maxValue) { + if (expression == null || expression.trim().isEmpty()) { + throw new IllegalArgumentException("Expression cannot be null or empty"); + } + + if (maxValue <= 0 || maxValue > 10000) { + throw new IllegalArgumentException("maxValue must be between 1 and 10000 for safety"); + } + List results = new ArrayList<>(); DoubleEvaluator evaluator = new DoubleEvaluator(); - // Validate the expression - if (!expression.matches("[0-9n+\\-*/() ]+")) { - throw new IllegalArgumentException("Invalid expression"); + // Validate the expression format + if (!RegexPatternUtils.getInstance() + .getMathExpressionPattern() + .matcher(expression.trim()) + .matches()) { + throw new IllegalArgumentException("Invalid expression format: " + expression); } for (int n = 1; n <= maxValue; n++) { - // Replace 'n' with the current value of n, correctly handling numbers before - // 'n' - String sanitizedExpression = sanitizeNFunction(expression, n); - Double result = evaluator.evaluate(sanitizedExpression); + try { + // Replace 'n' with the current value of n, correctly handling numbers before 'n' + String sanitizedExpression = sanitizeNFunction(expression.trim(), n); + Double result = evaluator.evaluate(sanitizedExpression); - // Check if the result is null or not within bounds - if (result == null) break; + // Check if the result is null or not within bounds + if (result == null || !Double.isFinite(result)) { + continue; + } - if (result.intValue() > 0 && result.intValue() <= maxValue) - results.add(result.intValue()); + int intResult = result.intValue(); + if (intResult > 0 && intResult <= maxValue) { + results.add(intResult); + } + } catch (Exception e) { + log.debug( + "Failed to evaluate expression '{}' for n={}: {}", + expression, + n, + e.getMessage()); + // Continue with next value instead of breaking + } } return results; } - private static String sanitizeNFunction(String expression, int nValue) { - String sanitizedExpression = expression.replace(" ", ""); + private String sanitizeNFunction(String expression, int nValue) { + // Remove all spaces using a specialized character removal + StringBuilder sb = new StringBuilder(expression.length()); + for (int i = 0; i < expression.length(); i++) { + char c = expression.charAt(i); + if (c != ' ') { + sb.append(c); + } + } + String sanitizedExpression = sb.toString(); String multiplyByOpeningRoundBracketPattern = "([0-9n)])\\("; // example: n(n-1), 9(n-1), (n-1)(n-2) sanitizedExpression = @@ -319,23 +583,45 @@ public class GeneralUtils { return sanitizedExpression; } - private static String insertMultiplicationBeforeN(String expression, int nValue) { - // Insert multiplication between a number and 'n' (e.g., "4n" becomes "4*n") - String withMultiplication = expression.replaceAll("(\\d)n", "$1*n"); + private String insertMultiplicationBeforeN(String expression, int nValue) { + // Insert multiplication between a number and 'n' (e.g., "4n" becomes "4*n") using a loop + StringBuilder sb = new StringBuilder(expression.length() + 4); // +4 for possible extra '*' + for (int i = 0; i < expression.length(); i++) { + char c = expression.charAt(i); + sb.append(c); + if (Character.isDigit(c) + && i + 1 < expression.length() + && expression.charAt(i + 1) == 'n') { + sb.append('*'); + } + } + String withMultiplication = sb.toString(); withMultiplication = formatConsecutiveNsForNFunction(withMultiplication); // Now replace 'n' with its current value return withMultiplication.replace("n", String.valueOf(nValue)); } - private static String formatConsecutiveNsForNFunction(String expression) { + private String formatConsecutiveNsForNFunction(String expression) { String text = expression; - while (text.matches(".*n{2,}.*")) { - text = text.replaceAll("(? handlePart(String part, int totalPages, int offset) { + private List handlePart(String part, int totalPages, int offset) { List partResult = new ArrayList<>(); // First check for n-syntax because it should not be processed as a range @@ -361,7 +647,7 @@ public class GeneralUtils { } } } catch (NumberFormatException e) { - // Range is invalid, ignore this part + log.debug("Invalid range: {}", part); } } else { // This is a single page number @@ -370,14 +656,14 @@ public class GeneralUtils { if (pageNum >= 1 && pageNum <= totalPages) { partResult.add(pageNum - 1 + offset); } - } catch (NumberFormatException ignored) { - // Ignore invalid numbers + } catch (NumberFormatException e) { + log.debug("Invalid page number: {}", part); } } return partResult; } - public static boolean createDir(String path) { + public boolean createDir(String path) { Path folder = Paths.get(path); if (!Files.exists(folder)) { try { @@ -390,7 +676,7 @@ public class GeneralUtils { return true; } - public static boolean isValidUUID(String uuid) { + public boolean isValidUUID(String uuid) { if (uuid == null) { return false; } @@ -406,7 +692,7 @@ public class GeneralUtils { * Internal Implementation Details * *------------------------------------------------------------------------*/ - public static void saveKeyToSettings(String key, Object newValue) throws IOException { + public void saveKeyToSettings(String key, Object newValue) throws IOException { String[] keyArray = key.split("\\."); Path settingsPath = Paths.get(InstallationPathConfig.getSettingsPath()); YamlHelper settingsYaml = new YamlHelper(settingsPath); @@ -414,48 +700,67 @@ public class GeneralUtils { settingsYaml.saveOverride(settingsPath); } - public static String generateMachineFingerprint() { + /* + * Machine fingerprint generation with better error logging and fallbacks. + * + * @return unique machine fingerprint or "GenericID" if generation fails + */ + public String generateMachineFingerprint() { try { - // Get the MAC address StringBuilder sb = new StringBuilder(); + + // Try to get MAC address from primary network interface InetAddress ip = InetAddress.getLocalHost(); NetworkInterface network = NetworkInterface.getByInetAddress(ip); - if (network == null) { + if (network == null || network.getHardwareAddress() == null) { + // Fallback: iterate through all network interfaces Enumeration networks = NetworkInterface.getNetworkInterfaces(); while (networks.hasMoreElements()) { NetworkInterface net = networks.nextElement(); - byte[] mac = net.getHardwareAddress(); - if (mac != null) { - for (int i = 0; i < mac.length; i++) { - sb.append(String.format("%02X", mac[i])); + if (net.isUp() && !net.isLoopback() && !net.isVirtual()) { + byte[] mac = net.getHardwareAddress(); + if (mac != null && mac.length > 0) { + for (byte b : mac) { + sb.append(String.format("%02X", b)); + } + break; // Use the first valid network interface } - break; // Use the first network interface with a MAC address } } } else { byte[] mac = network.getHardwareAddress(); if (mac != null) { - for (int i = 0; i < mac.length; i++) { - sb.append(String.format("%02X", mac[i])); + for (byte b : mac) { + sb.append(String.format("%02X", b)); } } } - // Hash the MAC address for privacy and consistency + // If no MAC address found, use hostname as fallback + if (sb.length() == 0) { + String hostname = InetAddress.getLocalHost().getHostName(); + sb.append(hostname != null ? hostname : "unknown-host"); + log.warn("No MAC address found, using hostname for fingerprint generation"); + } + + // Hash the collected data for privacy and consistency MessageDigest md = MessageDigest.getInstance("SHA-256"); byte[] hash = md.digest(sb.toString().getBytes(StandardCharsets.UTF_8)); StringBuilder fingerprint = new StringBuilder(); for (byte b : hash) { fingerprint.append(String.format("%02x", b)); } + + log.debug("Successfully generated machine fingerprint"); return fingerprint.toString(); } catch (Exception e) { + log.warn("Failed to generate machine fingerprint: {}", e.getMessage()); return "GenericID"; } } - /** + /* * Extracts the default pipeline configurations from the classpath to the installation path. * Creates directories if needed and copies default JSON files. * @@ -464,7 +769,7 @@ public class GeneralUtils { * * @throws IOException if an I/O error occurs during file operations */ - public static void extractPipeline() throws IOException { + public void extractPipeline() throws IOException { Path pipelineDir = Paths.get(InstallationPathConfig.getPipelinePath(), DEFAULT_WEBUI_CONFIGS_DIR); Files.createDirectories(pipelineDir); @@ -486,7 +791,7 @@ public class GeneralUtils { } } - /** + /* * Extracts the specified Python script from the classpath to the installation path. Validates * name and copies file atomically when possible, overwriting existing. * @@ -497,7 +802,7 @@ public class GeneralUtils { * @throws IllegalArgumentException if the script name is invalid or not allowed * @throws IOException if an I/O error occurs */ - public static Path extractScript(String scriptName) throws IOException { + public Path extractScript(String scriptName) throws IOException { // Validate input if (scriptName == null || scriptName.trim().isEmpty()) { throw new IllegalArgumentException("scriptName must not be null or empty"); @@ -530,15 +835,14 @@ public class GeneralUtils { return target; } - /** + /* * Copies a resource from the classpath to a specified target file. * * @param resource the ClassPathResource to copy * @param target the target Path where the resource will be copied * @throws IOException if an I/O error occurs during the copy operation */ - private static void copyResourceToFile(ClassPathResource resource, Path target) - throws IOException { + private void copyResourceToFile(ClassPathResource resource, Path target) throws IOException { Path dir = target.getParent(); Path tmp = Files.createTempFile(dir, target.getFileName().toString(), ".tmp"); try (InputStream in = resource.getInputStream()) { @@ -573,7 +877,7 @@ public class GeneralUtils { } } - public static boolean isVersionHigher(String currentVersion, String compareVersion) { + public boolean isVersionHigher(String currentVersion, String compareVersion) { if (currentVersion == null || compareVersion == null) { return false; } @@ -601,4 +905,67 @@ public class GeneralUtils { // If all components so far are equal, the longer version is considered higher return current.length > compare.length; } + + /** + * Optimizes a PDF using Ghostscript with ebook settings for better e-reader compatibility. Uses + * -dPDFSETTINGS=/ebook -dFastWebView=true settings to create an optimized PDF. + * + * @param inputPdfBytes Original PDF as byte array + * @return Optimized PDF as byte array + * @throws IOException if Ghostscript optimization fails + */ + public byte[] optimizePdfWithGhostscript(byte[] inputPdfBytes) throws IOException { + Path tempInput = null; + Path tempOutput = null; + + try { + tempInput = Files.createTempFile("gs_input_", ".pdf"); + tempOutput = Files.createTempFile("gs_output_", ".pdf"); + + Files.write(tempInput, inputPdfBytes); + + List command = new ArrayList<>(); + command.add("gs"); + command.add("-sDEVICE=pdfwrite"); + command.add("-dPDFSETTINGS=/ebook"); + command.add("-dFastWebView=true"); + command.add("-dNOPAUSE"); + command.add("-dQUIET"); + command.add("-dBATCH"); + command.add("-sOutputFile=" + tempOutput.toString()); + command.add(tempInput.toString()); + + ProcessExecutor.ProcessExecutorResult result = + ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT) + .runCommandWithOutputHandling(command); + + if (result.getRc() != 0) { + log.warn( + "Ghostscript ebook optimization failed with return code: {}", + result.getRc()); + throw ExceptionUtils.createGhostscriptCompressionException(); + } + + return Files.readAllBytes(tempOutput); + + } catch (Exception e) { + log.warn("Ghostscript ebook optimization failed", e); + throw ExceptionUtils.createGhostscriptCompressionException(e); + } finally { + if (tempInput != null) { + try { + Files.deleteIfExists(tempInput); + } catch (IOException e) { + log.warn("Failed to delete temp input file: {}", tempInput, e); + } + } + if (tempOutput != null) { + try { + Files.deleteIfExists(tempOutput); + } catch (IOException e) { + log.warn("Failed to delete temp output file: {}", tempOutput, e); + } + } + } + } } diff --git a/app/common/src/main/java/stirling/software/common/util/ImageProcessingUtils.java b/app/common/src/main/java/stirling/software/common/util/ImageProcessingUtils.java index 7140b3cc2..c1b694515 100644 --- a/app/common/src/main/java/stirling/software/common/util/ImageProcessingUtils.java +++ b/app/common/src/main/java/stirling/software/common/util/ImageProcessingUtils.java @@ -85,19 +85,16 @@ public class ImageProcessingUtils { return 0; } int orientationTag = directory.getInt(ExifSubIFDDirectory.TAG_ORIENTATION); - switch (orientationTag) { - case 1: - return 0; - case 6: - return 90; - case 3: - return 180; - case 8: - return 270; - default: + return switch (orientationTag) { + case 1 -> 0; + case 6 -> 90; + case 3 -> 180; + case 8 -> 270; + default -> { log.warn("Unknown orientation tag: {}", orientationTag); - return 0; - } + yield 0; + } + }; } catch (ImageProcessingException | MetadataException e) { return 0; } diff --git a/app/common/src/main/java/stirling/software/common/util/PDFService.java b/app/common/src/main/java/stirling/software/common/util/PDFService.java new file mode 100644 index 000000000..255b4e214 --- /dev/null +++ b/app/common/src/main/java/stirling/software/common/util/PDFService.java @@ -0,0 +1,35 @@ +package stirling.software.common.util; + +import java.io.IOException; +import java.util.List; + +import org.apache.pdfbox.multipdf.PDFMergerUtility; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; + +import stirling.software.common.service.CustomPDFDocumentFactory; + +@Service +@RequiredArgsConstructor +public class PDFService { + + private final CustomPDFDocumentFactory pdfDocumentFactory; + + /* + * Merge multiple PDF documents into a single PDF document + * + * @param documents List of PDDocument to be merged + * @return Merged PDDocument + * @throws IOException If an error occurs during merging + */ + public PDDocument mergeDocuments(List documents) throws IOException { + PDDocument merged = pdfDocumentFactory.createNewDocument(); + PDFMergerUtility merger = new PDFMergerUtility(); + for (PDDocument doc : documents) { + merger.appendDocument(merged, doc); + } + return merged; + } +} diff --git a/app/common/src/main/java/stirling/software/common/util/PDFToFile.java b/app/common/src/main/java/stirling/software/common/util/PDFToFile.java index f763f5414..32f2cc874 100644 --- a/app/common/src/main/java/stirling/software/common/util/PDFToFile.java +++ b/app/common/src/main/java/stirling/software/common/util/PDFToFile.java @@ -25,18 +25,22 @@ import com.vladsch.flexmark.util.data.MutableDataSet; import io.github.pixee.security.Filenames; -import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult; @Slf4j -@NoArgsConstructor public class PDFToFile { + private final TempFileManager tempFileManager; + + public PDFToFile(TempFileManager tempFileManager) { + this.tempFileManager = tempFileManager; + } + public ResponseEntity processPdfToMarkdown(MultipartFile inputFile) throws IOException, InterruptedException { - if (!"application/pdf".equals(inputFile.getContentType())) { + if (!MediaType.APPLICATION_PDF_VALUE.equals(inputFile.getContentType())) { return new ResponseEntity<>(HttpStatus.BAD_REQUEST); } @@ -71,15 +75,12 @@ public class PDFToFile { pdfBaseName = originalPdfFileName.substring(0, originalPdfFileName.lastIndexOf('.')); } - Path tempInputFile = null; - Path tempOutputDir = null; byte[] fileBytes; - String fileName = "temp.file"; + String fileName; - try { - tempInputFile = Files.createTempFile("input_", ".pdf"); - inputFile.transferTo(tempInputFile); - tempOutputDir = Files.createTempDirectory("output_"); + try (TempFile tempInputFile = new TempFile(tempFileManager, ".pdf"); + TempDirectory tempOutputDir = new TempDirectory(tempFileManager)) { + inputFile.transferTo(tempInputFile.getFile()); List command = new ArrayList<>( @@ -88,14 +89,16 @@ public class PDFToFile { "-s", "-noframes", "-c", - tempInputFile.toString(), + tempInputFile.getAbsolutePath(), pdfBaseName)); ProcessExecutorResult returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.PDFTOHTML) - .runCommandWithOutputHandling(command, tempOutputDir.toFile()); + .runCommandWithOutputHandling( + command, tempOutputDir.getPath().toFile()); // Process HTML files to Markdown - File[] outputFiles = Objects.requireNonNull(tempOutputDir.toFile().listFiles()); + File[] outputFiles = + Objects.requireNonNull(tempOutputDir.getPath().toFile().listFiles()); List markdownFiles = new ArrayList<>(); // Convert HTML files to Markdown @@ -105,7 +108,7 @@ public class PDFToFile { String markdown = htmlToMarkdownConverter.convert(html); String mdFileName = outputFile.getName().replace(".html", ".md"); - File mdFile = new File(tempOutputDir.toFile(), mdFileName); + File mdFile = new File(tempOutputDir.getPath().toFile(), mdFileName); Files.writeString(mdFile.toPath(), markdown); markdownFiles.add(mdFile); } @@ -142,10 +145,6 @@ public class PDFToFile { fileBytes = byteArrayOutputStream.toByteArray(); } - - } finally { - if (tempInputFile != null) Files.deleteIfExists(tempInputFile); - if (tempOutputDir != null) FileUtils.deleteDirectory(tempOutputDir.toFile()); } return WebResponseUtils.bytesToWebResponse( fileBytes, fileName, MediaType.APPLICATION_OCTET_STREAM); @@ -153,7 +152,7 @@ public class PDFToFile { public ResponseEntity processPdfToHtml(MultipartFile inputFile) throws IOException, InterruptedException { - if (!"application/pdf".equals(inputFile.getContentType())) { + if (!MediaType.APPLICATION_PDF_VALUE.equals(inputFile.getContentType())) { return new ResponseEntity<>(HttpStatus.BAD_REQUEST); } @@ -164,19 +163,18 @@ public class PDFToFile { pdfBaseName = originalPdfFileName.substring(0, originalPdfFileName.lastIndexOf('.')); } - Path tempInputFile = null; - Path tempOutputDir = null; byte[] fileBytes; - String fileName = "temp.file"; + String fileName; + + try (TempFile inputFileTemp = new TempFile(tempFileManager, ".pdf"); + TempDirectory outputDirTemp = new TempDirectory(tempFileManager)) { + + Path tempInputFile = inputFileTemp.getPath(); + Path tempOutputDir = outputDirTemp.getPath(); - try { // Save the uploaded file to a temporary location - tempInputFile = Files.createTempFile("input_", ".pdf"); inputFile.transferTo(tempInputFile); - // Prepare the output directory - tempOutputDir = Files.createTempDirectory("output_"); - // Run the pdftohtml command with complex output List command = new ArrayList<>( @@ -208,11 +206,6 @@ public class PDFToFile { log.error("Exception writing zip", e); } fileBytes = byteArrayOutputStream.toByteArray(); - - } finally { - // Clean up the temporary files - if (tempInputFile != null) Files.deleteIfExists(tempInputFile); - if (tempOutputDir != null) FileUtils.deleteDirectory(tempOutputDir.toFile()); } return WebResponseUtils.bytesToWebResponse( @@ -223,14 +216,14 @@ public class PDFToFile { MultipartFile inputFile, String outputFormat, String libreOfficeFilter) throws IOException, InterruptedException { - if (!"application/pdf".equals(inputFile.getContentType())) { + if (!MediaType.APPLICATION_PDF_VALUE.equals(inputFile.getContentType())) { return new ResponseEntity<>(HttpStatus.BAD_REQUEST); } // Get the original PDF file name without the extension String originalPdfFileName = Filenames.toSimpleFileName(inputFile.getOriginalFilename()); - if (originalPdfFileName == null || "".equals(originalPdfFileName.trim())) { + if (originalPdfFileName == null || originalPdfFileName.trim().isEmpty()) { originalPdfFileName = "output.pdf"; } // Assume file is pdf if no extension @@ -245,19 +238,18 @@ public class PDFToFile { return new ResponseEntity<>(HttpStatus.BAD_REQUEST); } - Path tempInputFile = null; - Path tempOutputDir = null; byte[] fileBytes; - String fileName = "temp.file"; + String fileName; + + try (TempFile inputFileTemp = new TempFile(tempFileManager, ".pdf"); + TempDirectory outputDirTemp = new TempDirectory(tempFileManager)) { + + Path tempInputFile = inputFileTemp.getPath(); + Path tempOutputDir = outputDirTemp.getPath(); - try { // Save the uploaded file to a temporary location - tempInputFile = Files.createTempFile("input_", ".pdf"); inputFile.transferTo(tempInputFile); - // Prepare the output directory - tempOutputDir = Files.createTempDirectory("output_"); - // Run the LibreOffice command List command = new ArrayList<>( @@ -308,11 +300,6 @@ public class PDFToFile { fileBytes = byteArrayOutputStream.toByteArray(); } - - } finally { - // Clean up the temporary files - Files.deleteIfExists(tempInputFile); - if (tempOutputDir != null) FileUtils.deleteDirectory(tempOutputDir.toFile()); } return WebResponseUtils.bytesToWebResponse( fileBytes, fileName, MediaType.APPLICATION_OCTET_STREAM); diff --git a/app/common/src/main/java/stirling/software/common/util/PdfAttachmentHandler.java b/app/common/src/main/java/stirling/software/common/util/PdfAttachmentHandler.java index 2478aad94..3bffb0401 100644 --- a/app/common/src/main/java/stirling/software/common/util/PdfAttachmentHandler.java +++ b/app/common/src/main/java/stirling/software/common/util/PdfAttachmentHandler.java @@ -8,7 +8,9 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; -import java.text.SimpleDateFormat; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Base64; import java.util.Date; @@ -18,7 +20,6 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; -import java.util.TimeZone; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -37,6 +38,7 @@ import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream; import org.apache.pdfbox.text.PDFTextStripper; import org.apache.pdfbox.text.TextPosition; import org.jetbrains.annotations.NotNull; +import org.springframework.http.MediaType; import org.springframework.web.multipart.MultipartFile; import lombok.Data; @@ -118,7 +120,7 @@ public class PdfAttachmentHandler { public String getContentType() { return attachment.getContentType() != null ? attachment.getContentType() - : "application/octet-stream"; + : MediaType.APPLICATION_OCTET_STREAM_VALUE; } @Override @@ -256,10 +258,7 @@ public class PdfAttachmentHandler { if (contentIdMap.isEmpty()) return htmlContent; - Pattern cidPattern = - Pattern.compile( - "(?i)]*\\ssrc\\s*=\\s*['\"]cid:([^'\"]+)['\"][^>]*>", - Pattern.CASE_INSENSITIVE); + Pattern cidPattern = RegexPatternUtils.getInstance().getInlineCidImagePattern(); Matcher matcher = cidPattern.matcher(htmlContent); StringBuilder result = new StringBuilder(); @@ -289,11 +288,15 @@ public class PdfAttachmentHandler { public static String formatEmailDate(Date date) { if (date == null) return ""; + return formatEmailDate(ZonedDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault())); + } - SimpleDateFormat formatter = - new SimpleDateFormat("EEE, MMM d, yyyy 'at' h:mm a z", Locale.ENGLISH); - formatter.setTimeZone(TimeZone.getTimeZone("UTC")); - return formatter.format(date); + public static String formatEmailDate(ZonedDateTime dateTime) { + if (dateTime == null) return ""; + + DateTimeFormatter formatter = + DateTimeFormatter.ofPattern("EEE, MMM d, yyyy 'at' h:mm a z", Locale.ENGLISH); + return dateTime.withZoneSameInstant(ZoneId.of("UTC")).format(formatter); } @Data @@ -313,121 +316,20 @@ public class PdfAttachmentHandler { } } - public static class AttachmentMarkerPositionFinder extends PDFTextStripper { - @Getter private final List positions = new ArrayList<>(); - private int currentPageIndex; - protected boolean sortByPosition; - private boolean isInAttachmentSection; - private boolean attachmentSectionFound; - private final StringBuilder currentText = new StringBuilder(); - - private static final Pattern ATTACHMENT_SECTION_PATTERN = - Pattern.compile("attachments\\s*\\(\\d+\\)", Pattern.CASE_INSENSITIVE); - - private static final Pattern FILENAME_PATTERN = - Pattern.compile("@\\s*([^\\s\\(]+(?:\\.[a-zA-Z0-9]+)?)"); - - public AttachmentMarkerPositionFinder() { - super(); - this.currentPageIndex = 0; - this.sortByPosition = false; // Disable sorting to preserve document order - this.isInAttachmentSection = false; - this.attachmentSectionFound = false; - } - - @Override - public String getText(PDDocument document) throws IOException { - super.getText(document); - - if (sortByPosition) { - positions.sort( - (a, b) -> { - int pageCompare = Integer.compare(a.getPageIndex(), b.getPageIndex()); - if (pageCompare != 0) return pageCompare; - return Float.compare( - b.getY(), a.getY()); // Descending Y per PDF coordinate system - }); - } - - return ""; // Return empty string as we only need positions - } - - @Override - protected void startPage(PDPage page) throws IOException { - super.startPage(page); - } - - @Override - protected void endPage(PDPage page) throws IOException { - currentPageIndex++; - super.endPage(page); - } - - @Override - protected void writeString(String string, List textPositions) - throws IOException { - String lowerString = string.toLowerCase(); - - if (ATTACHMENT_SECTION_PATTERN.matcher(lowerString).find()) { - isInAttachmentSection = true; - attachmentSectionFound = true; - } - - if (isInAttachmentSection - && (lowerString.contains("") - || lowerString.contains("") - || (attachmentSectionFound - && lowerString.trim().isEmpty() - && string.length() > 50))) { - isInAttachmentSection = false; - } - - if (isInAttachmentSection) { - currentText.append(string); - - for (int i = 0; (i = string.indexOf(ATTACHMENT_MARKER, i)) != -1; i++) { - if (i < textPositions.size()) { - TextPosition textPosition = textPositions.get(i); - - String filename = extractFilenameAfterMarker(string, i); - - MarkerPosition position = - new MarkerPosition( - currentPageIndex, - textPosition.getXDirAdj(), - textPosition.getYDirAdj(), - ATTACHMENT_MARKER, - filename); - positions.add(position); - } - } - } - super.writeString(string, textPositions); - } - - @Override - public void setSortByPosition(boolean sortByPosition) { - this.sortByPosition = sortByPosition; - } - - private String extractFilenameAfterMarker(String text, int markerIndex) { - String afterMarker = text.substring(markerIndex + 1); - - Matcher matcher = FILENAME_PATTERN.matcher("@" + afterMarker); - if (matcher.find()) { - return matcher.group(1); - } - - String[] parts = afterMarker.split("[\\s\\(\\)]+"); - for (String part : parts) { - part = part.trim(); - if (part.length() > 3 && part.contains(".")) { - return part; - } - } - - return null; - } + private static String normalizeFilename(String filename) { + if (filename == null) return ""; + String normalized = filename.toLowerCase().trim(); + normalized = + RegexPatternUtils.getInstance() + .getWhitespacePattern() + .matcher(normalized) + .replaceAll(" "); + normalized = + RegexPatternUtils.getInstance() + .getPattern("[^a-zA-Z0-9._-]") + .matcher(normalized) + .replaceAll(""); + return normalized; } private static Map addAttachmentsToDocumentWithMapping( @@ -607,12 +509,122 @@ public class PdfAttachmentHandler { return null; } - private static String normalizeFilename(String filename) { - if (filename == null) return ""; - return filename.toLowerCase() - .trim() - .replaceAll("\\s+", " ") - .replaceAll("[^a-zA-Z0-9._-]", ""); + public static class AttachmentMarkerPositionFinder extends PDFTextStripper { + private static final Pattern ATTACHMENT_SECTION_PATTERN = + RegexPatternUtils.getInstance().getAttachmentSectionPattern(); + private static final Pattern FILENAME_PATTERN = + RegexPatternUtils.getInstance().getAttachmentFilenamePattern(); + @Getter private final List positions = new ArrayList<>(); + private final StringBuilder currentText = new StringBuilder(); + protected boolean sortByPosition; + private int currentPageIndex; + private boolean isInAttachmentSection; + private boolean attachmentSectionFound; + + public AttachmentMarkerPositionFinder() { + super(); + this.currentPageIndex = 0; + this.sortByPosition = false; // Disable sorting to preserve document order + this.isInAttachmentSection = false; + this.attachmentSectionFound = false; + } + + @Override + public String getText(PDDocument document) throws IOException { + super.getText(document); + + if (sortByPosition) { + positions.sort( + (a, b) -> { + int pageCompare = Integer.compare(a.getPageIndex(), b.getPageIndex()); + if (pageCompare != 0) return pageCompare; + return Float.compare( + b.getY(), a.getY()); // Descending Y per PDF coordinate system + }); + } + + return ""; // Return empty string as we only need positions + } + + @Override + protected void startPage(PDPage page) throws IOException { + super.startPage(page); + } + + @Override + protected void endPage(PDPage page) throws IOException { + currentPageIndex++; + super.endPage(page); + } + + @Override + protected void writeString(String string, List textPositions) + throws IOException { + String lowerString = string.toLowerCase(); + + if (ATTACHMENT_SECTION_PATTERN.matcher(lowerString).find()) { + isInAttachmentSection = true; + attachmentSectionFound = true; + } + + if (isInAttachmentSection + && (lowerString.contains("") + || lowerString.contains("") + || (attachmentSectionFound + && lowerString.trim().isEmpty() + && string.length() > 50))) { + isInAttachmentSection = false; + } + + if (isInAttachmentSection) { + currentText.append(string); + + for (int i = 0; (i = string.indexOf(ATTACHMENT_MARKER, i)) != -1; i++) { + if (i < textPositions.size()) { + TextPosition textPosition = textPositions.get(i); + + String filename = extractFilenameAfterMarker(string, i); + + MarkerPosition position = + new MarkerPosition( + currentPageIndex, + textPosition.getXDirAdj(), + textPosition.getYDirAdj(), + ATTACHMENT_MARKER, + filename); + positions.add(position); + } + } + } + super.writeString(string, textPositions); + } + + @Override + public void setSortByPosition(boolean sortByPosition) { + this.sortByPosition = sortByPosition; + } + + private String extractFilenameAfterMarker(String text, int markerIndex) { + String afterMarker = text.substring(markerIndex + 1); + + Matcher matcher = FILENAME_PATTERN.matcher("@" + afterMarker); + if (matcher.find()) { + return matcher.group(1); + } + + String[] parts = + RegexPatternUtils.getInstance() + .getWhitespaceParenthesesSplitPattern() + .split(afterMarker); + for (String part : parts) { + part = part.trim(); + if (part.length() > 3 && part.contains(".")) { + return part; + } + } + + return null; + } } private static void addAttachmentAnnotationToPageWithMapping( diff --git a/app/common/src/main/java/stirling/software/common/util/PdfToCbrUtils.java b/app/common/src/main/java/stirling/software/common/util/PdfToCbrUtils.java new file mode 100644 index 000000000..cb17429ff --- /dev/null +++ b/app/common/src/main/java/stirling/software/common/util/PdfToCbrUtils.java @@ -0,0 +1,173 @@ +package stirling.software.common.util; + +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +import javax.imageio.ImageIO; + +import org.apache.commons.io.FilenameUtils; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.rendering.ImageType; +import org.apache.pdfbox.rendering.PDFRenderer; +import org.springframework.web.multipart.MultipartFile; + +import lombok.extern.slf4j.Slf4j; + +import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult; + +@Slf4j +public class PdfToCbrUtils { + + public static byte[] convertPdfToCbr( + MultipartFile pdfFile, int dpi, CustomPDFDocumentFactory pdfDocumentFactory) + throws IOException { + + validatePdfFile(pdfFile); + + try (PDDocument document = pdfDocumentFactory.load(pdfFile)) { + if (document.getNumberOfPages() == 0) { + throw new IllegalArgumentException("PDF file contains no pages"); + } + + return createCbrFromPdf(document, dpi); + } + } + + private static void validatePdfFile(MultipartFile file) { + if (file == null || file.isEmpty()) { + throw new IllegalArgumentException("File cannot be null or empty"); + } + + String filename = file.getOriginalFilename(); + if (filename == null) { + throw new IllegalArgumentException("File must have a name"); + } + + String extension = FilenameUtils.getExtension(filename).toLowerCase(); + if (!"pdf".equals(extension)) { + throw new IllegalArgumentException("File must be a PDF"); + } + } + + private static byte[] createCbrFromPdf(PDDocument document, int dpi) throws IOException { + PDFRenderer pdfRenderer = new PDFRenderer(document); + + Path tempDir = Files.createTempDirectory("stirling-pdf-cbr-"); + List generatedImages = new ArrayList<>(); + try { + int totalPages = document.getNumberOfPages(); + + for (int pageIndex = 0; pageIndex < totalPages; pageIndex++) { + try { + BufferedImage image = + pdfRenderer.renderImageWithDPI(pageIndex, dpi, ImageType.RGB); + + String imageFilename = String.format("page_%03d.png", pageIndex + 1); + Path imagePath = tempDir.resolve(imageFilename); + + ImageIO.write(image, "PNG", imagePath.toFile()); + generatedImages.add(imagePath); + + } catch (IOException e) { + log.warn("Error processing page {}: {}", pageIndex + 1, e.getMessage()); + } catch (OutOfMemoryError e) { + throw ExceptionUtils.createOutOfMemoryDpiException(pageIndex + 1, dpi, e); + } catch (NegativeArraySizeException e) { + throw ExceptionUtils.createOutOfMemoryDpiException(pageIndex + 1, dpi, e); + } + } + + if (generatedImages.isEmpty()) { + throw new IOException("Failed to render any pages to images for CBR conversion"); + } + + return createRarArchive(tempDir, generatedImages); + } finally { + cleanupTempFiles(generatedImages, tempDir); + } + } + + private static byte[] createRarArchive(Path tempDir, List images) throws IOException { + List command = new ArrayList<>(); + command.add("rar"); + command.add("a"); + command.add("-m5"); + command.add("-ep1"); + + Path rarFile = tempDir.resolve("output.cbr"); + command.add(rarFile.getFileName().toString()); + + for (Path image : images) { + command.add(image.getFileName().toString()); + } + + ProcessExecutor executor = + ProcessExecutor.getInstance(ProcessExecutor.Processes.INSTALL_APP); + try { + ProcessExecutorResult result = + executor.runCommandWithOutputHandling(command, tempDir.toFile()); + if (result.getRc() != 0) { + throw new IOException("RAR command failed: " + result.getMessages()); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException("RAR command interrupted", e); + } + + if (!Files.exists(rarFile)) { + throw new IOException("RAR file was not created"); + } + + try (FileInputStream fis = new FileInputStream(rarFile.toFile()); + ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + fis.transferTo(baos); + return baos.toByteArray(); + } + } + + private static void cleanupTempFiles(List images, Path tempDir) { + for (Path image : images) { + try { + Files.deleteIfExists(image); + } catch (IOException e) { + log.warn("Failed to delete temp image file {}: {}", image, e.getMessage()); + } + } + if (tempDir != null) { + try (var paths = Files.walk(tempDir)) { + paths.sorted(Comparator.reverseOrder()) + .forEach( + path -> { + try { + Files.deleteIfExists(path); + } catch (IOException e) { + log.warn( + "Failed to delete temp path {}: {}", + path, + e.getMessage()); + } + }); + } catch (IOException e) { + log.warn("Failed to clean up temp directory {}: {}", tempDir, e.getMessage()); + } + } + } + + public static boolean isPdfFile(MultipartFile file) { + String filename = file.getOriginalFilename(); + if (filename == null) { + return false; + } + + String extension = FilenameUtils.getExtension(filename).toLowerCase(); + return "pdf".equals(extension); + } +} diff --git a/app/common/src/main/java/stirling/software/common/util/PdfToCbzUtils.java b/app/common/src/main/java/stirling/software/common/util/PdfToCbzUtils.java new file mode 100644 index 000000000..b1b7b5b8c --- /dev/null +++ b/app/common/src/main/java/stirling/software/common/util/PdfToCbzUtils.java @@ -0,0 +1,99 @@ +package stirling.software.common.util; + +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import javax.imageio.ImageIO; + +import org.apache.commons.io.FilenameUtils; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.rendering.ImageType; +import org.apache.pdfbox.rendering.PDFRenderer; +import org.springframework.web.multipart.MultipartFile; + +import lombok.extern.slf4j.Slf4j; + +import stirling.software.common.service.CustomPDFDocumentFactory; + +@Slf4j +public class PdfToCbzUtils { + + public static byte[] convertPdfToCbz( + MultipartFile pdfFile, int dpi, CustomPDFDocumentFactory pdfDocumentFactory) + throws IOException { + + validatePdfFile(pdfFile); + + try (PDDocument document = pdfDocumentFactory.load(pdfFile)) { + if (document.getNumberOfPages() == 0) { + throw new IllegalArgumentException("PDF file contains no pages"); + } + + return createCbzFromPdf(document, dpi); + } + } + + private static void validatePdfFile(MultipartFile file) { + if (file == null || file.isEmpty()) { + throw new IllegalArgumentException("File cannot be null or empty"); + } + + String filename = file.getOriginalFilename(); + if (filename == null) { + throw new IllegalArgumentException("File must have a name"); + } + + String extension = FilenameUtils.getExtension(filename).toLowerCase(); + if (!"pdf".equals(extension)) { + throw new IllegalArgumentException("File must be a PDF"); + } + } + + private static byte[] createCbzFromPdf(PDDocument document, int dpi) throws IOException { + PDFRenderer pdfRenderer = new PDFRenderer(document); + + try (ByteArrayOutputStream cbzOutputStream = new ByteArrayOutputStream(); + ZipOutputStream zipOut = new ZipOutputStream(cbzOutputStream)) { + + int totalPages = document.getNumberOfPages(); + + for (int pageIndex = 0; pageIndex < totalPages; pageIndex++) { + try { + BufferedImage image = + pdfRenderer.renderImageWithDPI(pageIndex, dpi, ImageType.RGB); + + String imageFilename = String.format("page_%03d.png", pageIndex + 1); + + ZipEntry zipEntry = new ZipEntry(imageFilename); + zipOut.putNextEntry(zipEntry); + + ImageIO.write(image, "PNG", zipOut); + zipOut.closeEntry(); + + } catch (IOException e) { + log.warn("Error processing page {}: {}", pageIndex + 1, e.getMessage()); + } catch (OutOfMemoryError e) { + throw ExceptionUtils.createOutOfMemoryDpiException(pageIndex + 1, dpi, e); + } catch (NegativeArraySizeException e) { + throw ExceptionUtils.createOutOfMemoryDpiException(pageIndex + 1, dpi, e); + } + } + + zipOut.finish(); + return cbzOutputStream.toByteArray(); + } + } + + public static boolean isPdfFile(MultipartFile file) { + String filename = file.getOriginalFilename(); + if (filename == null) { + return false; + } + + String extension = FilenameUtils.getExtension(filename).toLowerCase(); + return "pdf".equals(extension); + } +} diff --git a/app/common/src/main/java/stirling/software/common/util/PdfUtils.java b/app/common/src/main/java/stirling/software/common/util/PdfUtils.java index 6f4305bd3..842baffcb 100644 --- a/app/common/src/main/java/stirling/software/common/util/PdfUtils.java +++ b/app/common/src/main/java/stirling/software/common/util/PdfUtils.java @@ -8,6 +8,8 @@ import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @@ -29,19 +31,25 @@ import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; import org.apache.pdfbox.rendering.ImageType; import org.apache.pdfbox.rendering.PDFRenderer; import org.apache.pdfbox.text.PDFTextStripper; +import org.springframework.http.MediaType; import org.springframework.web.multipart.MultipartFile; import io.github.pixee.security.Filenames; +import lombok.experimental.UtilityClass; import lombok.extern.slf4j.Slf4j; import stirling.software.common.model.ApplicationProperties; import stirling.software.common.service.CustomPDFDocumentFactory; @Slf4j +@UtilityClass public class PdfUtils { - public static PDRectangle textToPageSize(String size) { + private final RegexPatternUtils patternCache = RegexPatternUtils.getInstance(); + + public PDRectangle textToPageSize(String size) { + switch (size.toUpperCase()) { case "A0" -> { return PDRectangle.A0; @@ -74,7 +82,7 @@ public class PdfUtils { } } - public static List getAllImages(PDResources resources) throws IOException { + public List getAllImages(PDResources resources) throws IOException { List images = new ArrayList<>(); for (COSName name : resources.getXObjectNames()) { @@ -91,7 +99,7 @@ public class PdfUtils { return images; } - public static boolean hasImages(PDDocument document, String pagesToCheck) throws IOException { + public boolean hasImages(PDDocument document, String pagesToCheck) throws IOException { String[] pageOrderArr = pagesToCheck.split(","); List pageList = GeneralUtils.parsePageList(pageOrderArr, document.getNumberOfPages()); @@ -106,7 +114,7 @@ public class PdfUtils { return false; } - public static boolean hasText(PDDocument document, String pageNumbersToCheck, String phrase) + public boolean hasText(PDDocument document, String pageNumbersToCheck, String phrase) throws IOException { String[] pageOrderArr = pageNumbersToCheck.split(","); List pageList = @@ -122,11 +130,11 @@ public class PdfUtils { return false; } - public static boolean hasImagesOnPage(PDPage page) throws IOException { - return getAllImages(page.getResources()).size() > 0; + public boolean hasImagesOnPage(PDPage page) throws IOException { + return !getAllImages(page.getResources()).isEmpty(); } - public static boolean hasTextOnPage(PDPage page, String phrase) throws IOException { + public boolean hasTextOnPage(PDPage page, String phrase) throws IOException { PDFTextStripper textStripper = new PDFTextStripper(); PDDocument tempDoc = new PDDocument(); tempDoc.addPage(page); @@ -135,14 +143,15 @@ public class PdfUtils { return pageText.contains(phrase); } - public static byte[] convertFromPdf( + public byte[] convertFromPdf( CustomPDFDocumentFactory pdfDocumentFactory, byte[] inputStream, String imageType, ImageType colorType, boolean singleImage, int DPI, - String filename) + String filename, + boolean includeAnnotations) throws IOException, Exception { // Validate and limit DPI to prevent excessive memory usage @@ -155,7 +164,8 @@ public class PdfUtils { if (DPI > maxSafeDpi) { throw ExceptionUtils.createIllegalArgumentException( "error.dpiExceedsLimit", - "DPI value {0} exceeds maximum safe limit of {1}. High DPI values can cause memory issues and crashes. Please use a lower DPI value.", + "DPI value {0} exceeds maximum safe limit of {1}. High DPI values can cause" + + " memory issues and crashes. Please use a lower DPI value.", DPI, maxSafeDpi); } @@ -163,6 +173,9 @@ public class PdfUtils { try (PDDocument document = pdfDocumentFactory.load(inputStream)) { PDFRenderer pdfRenderer = new PDFRenderer(document); pdfRenderer.setSubsamplingAllowed(true); + if (!includeAnnotations) { + pdfRenderer.setAnnotationsFilter(annotation -> false); + } int pageCount = document.getNumberOfPages(); // Create a ByteArrayOutputStream to save the image(s) to @@ -192,11 +205,17 @@ public class PdfUtils { .contains("Maximum size of image exceeded")) { throw ExceptionUtils.createIllegalArgumentException( "error.pageTooBigForDpi", - "PDF page {0} is too large to render at {1} DPI. Please try a lower DPI value (recommended: 150 or less).", + "PDF page {0} is too large to render at {1} DPI. Please" + + " try a lower DPI value (recommended: 150 or" + + " less).", i + 1, DPI); } throw e; + } catch (OutOfMemoryError e) { + throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e); + } catch (NegativeArraySizeException e) { + throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e); } writer.writeToSequence(new IIOImage(image, null, null), param); } @@ -237,11 +256,18 @@ public class PdfUtils { .contains("Maximum size of image exceeded")) { throw ExceptionUtils.createIllegalArgumentException( "error.pageTooBigExceedsArray", - "PDF page {0} is too large to render at {1} DPI. The resulting image would exceed Java's maximum array size. Please try a lower DPI value (recommended: 150 or less).", + "PDF page {0} is too large to render at {1} DPI. The" + + " resulting image would exceed Java's maximum" + + " array size. Please try a lower DPI value" + + " (recommended: 150 or less).", i + 1, DPI); } throw e; + } catch (OutOfMemoryError e) { + throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e); + } catch (NegativeArraySizeException e) { + throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e); } pdfSizeImageIndex = i; dimension = @@ -278,11 +304,17 @@ public class PdfUtils { .contains("Maximum size of image exceeded")) { throw ExceptionUtils.createIllegalArgumentException( "error.pageTooBigForDpi", - "PDF page {0} is too large to render at {1} DPI. Please try a lower DPI value (recommended: 150 or less).", + "PDF page {0} is too large to render at {1} DPI. Please" + + " try a lower DPI value (recommended: 150 or" + + " less).", i + 1, DPI); } throw e; + } catch (OutOfMemoryError e) { + throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e); + } catch (NegativeArraySizeException e) { + throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e); } } @@ -311,11 +343,16 @@ public class PdfUtils { && e.getMessage().contains("Maximum size of image exceeded")) { throw ExceptionUtils.createIllegalArgumentException( "error.pageTooBigForDpi", - "PDF page {0} is too large to render at {1} DPI. Please try a lower DPI value (recommended: 150 or less).", + "PDF page {0} is too large to render at {1} DPI. Please try" + + " a lower DPI value (recommended: 150 or less).", i + 1, DPI); } throw e; + } catch (OutOfMemoryError e) { + throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e); + } catch (NegativeArraySizeException e) { + throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e); } try (ByteArrayOutputStream baosImage = new ByteArrayOutputStream()) { ImageIO.write(image, imageType, baosImage); @@ -349,23 +386,38 @@ public class PdfUtils { * @return converted document to PDF-Image * @throws IOException if conversion fails */ - public static PDDocument convertPdfToPdfImage(PDDocument document) throws IOException { + public PDDocument convertPdfToPdfImage(PDDocument document) throws IOException { PDDocument imageDocument = new PDDocument(); PDFRenderer pdfRenderer = new PDFRenderer(document); pdfRenderer.setSubsamplingAllowed(true); for (int page = 0; page < document.getNumberOfPages(); ++page) { BufferedImage bim; + + // Use global maximum DPI setting, fallback to 300 if not set + int renderDpi = 300; // Default fallback + ApplicationProperties properties = + ApplicationContextProvider.getBean(ApplicationProperties.class); + if (properties != null && properties.getSystem() != null) { + renderDpi = properties.getSystem().getMaxDPI(); + } + try { - bim = pdfRenderer.renderImageWithDPI(page, 300, ImageType.RGB); + bim = pdfRenderer.renderImageWithDPI(page, renderDpi, ImageType.RGB); } catch (IllegalArgumentException e) { if (e.getMessage() != null && e.getMessage().contains("Maximum size of image exceeded")) { throw ExceptionUtils.createIllegalArgumentException( "error.pageTooBigFor300Dpi", - "PDF page {0} is too large to render at 300 DPI. The resulting image would exceed Java's maximum array size. Please use a lower DPI value for PDF-to-image conversion.", + "PDF page {0} is too large to render at 300 DPI. The resulting image" + + " would exceed Java's maximum array size. Please use a lower DPI" + + " value for PDF-to-image conversion.", page + 1); } throw e; + } catch (OutOfMemoryError e) { + throw ExceptionUtils.createOutOfMemoryDpiException(page + 1, 300, e); + } catch (NegativeArraySizeException e) { + throw ExceptionUtils.createOutOfMemoryDpiException(page + 1, 300, e); } PDPage originalPage = document.getPage(page); @@ -383,8 +435,7 @@ public class PdfUtils { return imageDocument; } - private static BufferedImage prepareImageForPdfToImage( - int maxWidth, int height, String imageType) { + private BufferedImage prepareImageForPdfToImage(int maxWidth, int height, String imageType) { BufferedImage combined; if ("png".equalsIgnoreCase(imageType)) { combined = new BufferedImage(maxWidth, height, BufferedImage.TYPE_INT_ARGB); @@ -400,7 +451,7 @@ public class PdfUtils { return combined; } - public static byte[] imageToPdf( + public byte[] imageToPdf( MultipartFile[] files, String fitOption, boolean autoRotate, @@ -431,7 +482,7 @@ public class PdfUtils { ImageProcessingUtils.convertColorType(image, colorType); // Use JPEGFactory if it's JPEG since JPEG is lossy PDImageXObject pdImage = - (contentType != null && "image/jpeg".equals(contentType)) + (contentType != null && MediaType.IMAGE_JPEG_VALUE.equals(contentType)) ? JPEGFactory.createFromImage(doc, convertedImage) : LosslessFactory.createFromImage(doc, convertedImage); addImageToDocument(doc, pdImage, fitOption, autoRotate); @@ -444,7 +495,7 @@ public class PdfUtils { } } - public static void addImageToDocument( + public void addImageToDocument( PDDocument doc, PDImageXObject image, String fitOption, boolean autoRotate) throws IOException { boolean imageIsLandscape = image.getWidth() > image.getHeight(); @@ -494,7 +545,7 @@ public class PdfUtils { } } - public static byte[] overlayImage( + public byte[] overlayImage( CustomPDFDocumentFactory pdfDocumentFactory, byte[] pdfBytes, byte[] imageBytes, @@ -536,13 +587,16 @@ public class PdfUtils { public boolean containsTextInFile(PDDocument pdfDocument, String text, String pagesToCheck) throws IOException { PDFTextStripper textStripper = new PDFTextStripper(); - String pdfText = ""; + StringBuilder pdfText = new StringBuilder(); if (pagesToCheck == null || "all".equals(pagesToCheck)) { - pdfText = textStripper.getText(pdfDocument); + pdfText = new StringBuilder(textStripper.getText(pdfDocument)); } else { - // remove whitespaces - pagesToCheck = pagesToCheck.replaceAll("\\s+", ""); + // remove whitespaces using cached pattern + Pattern whitespacePattern = + patternCache.getPattern(RegexPatternUtils.getWhitespaceRegex()); + Matcher whitespaceMatcher = whitespacePattern.matcher(pagesToCheck); + pagesToCheck = whitespaceMatcher.replaceAll(""); String[] splitPoints = pagesToCheck.split(","); for (String splitPoint : splitPoints) { @@ -555,21 +609,21 @@ public class PdfUtils { for (int i = startPage; i <= endPage; i++) { textStripper.setStartPage(i); textStripper.setEndPage(i); - pdfText += textStripper.getText(pdfDocument); + pdfText.append(textStripper.getText(pdfDocument)); } } else { // Handle individual page int page = Integer.parseInt(splitPoint); textStripper.setStartPage(page); textStripper.setEndPage(page); - pdfText += textStripper.getText(pdfDocument); + pdfText.append(textStripper.getText(pdfDocument)); } } } pdfDocument.close(); - return pdfText.contains(text); + return pdfText.toString().contains(text); } public boolean pageCount(PDDocument pdfDocument, int pageCount, String comparator) @@ -577,16 +631,13 @@ public class PdfUtils { int actualPageCount = pdfDocument.getNumberOfPages(); pdfDocument.close(); - switch (comparator.toLowerCase()) { - case "greater": - return actualPageCount > pageCount; - case "equal": - return actualPageCount == pageCount; - case "less": - return actualPageCount < pageCount; - default: - throw ExceptionUtils.createInvalidArgumentException("comparator", comparator); - } + return switch (comparator.toLowerCase()) { + case "greater" -> actualPageCount > pageCount; + case "equal" -> actualPageCount == pageCount; + case "less" -> actualPageCount < pageCount; + default -> + throw ExceptionUtils.createInvalidArgumentException("comparator", comparator); + }; } public boolean pageSize(PDDocument pdfDocument, String expectedPageSize) throws IOException { diff --git a/app/common/src/main/java/stirling/software/common/util/ProcessExecutor.java b/app/common/src/main/java/stirling/software/common/util/ProcessExecutor.java index ee7297153..514e16212 100644 --- a/app/common/src/main/java/stirling/software/common/util/ProcessExecutor.java +++ b/app/common/src/main/java/stirling/software/common/util/ProcessExecutor.java @@ -15,6 +15,8 @@ import java.util.concurrent.TimeUnit; import io.github.pixee.security.BoundedLineReader; +import lombok.Getter; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; import stirling.software.common.model.ApplicationProperties; @@ -165,7 +167,7 @@ public class ProcessExecutor { semaphore.acquire(); try { - log.info("Running command: " + String.join(" ", command)); + log.info("Running command: {}", String.join(" ", command)); ProcessBuilder processBuilder = new ProcessBuilder(command); // Use the working directory if it's set @@ -250,7 +252,7 @@ public class ProcessExecutor { String outputMessage = String.join("\n", outputLines); messages += outputMessage; if (!liveUpdates) { - log.info("Command output:\n" + outputMessage); + log.info("Command output:\n{}", outputMessage); } } @@ -258,7 +260,7 @@ public class ProcessExecutor { String errorMessage = String.join("\n", errorLines); messages += errorMessage; if (!liveUpdates) { - log.warn("Command error output:\n" + errorMessage); + log.warn("Command error output:\n{}", errorMessage); } if (exitCode != 0) { if (isQpdf && exitCode == 3) { @@ -303,6 +305,8 @@ public class ProcessExecutor { OCR_MY_PDF } + @Setter + @Getter public class ProcessExecutorResult { int rc; String messages; @@ -311,21 +315,5 @@ public class ProcessExecutor { this.rc = rc; this.messages = messages; } - - public int getRc() { - return rc; - } - - public void setRc(int rc) { - this.rc = rc; - } - - public String getMessages() { - return messages; - } - - public void setMessages(String messages) { - this.messages = messages; - } } } diff --git a/app/common/src/main/java/stirling/software/common/util/RegexPatternUtils.java b/app/common/src/main/java/stirling/software/common/util/RegexPatternUtils.java new file mode 100644 index 000000000..8858c99bf --- /dev/null +++ b/app/common/src/main/java/stirling/software/common/util/RegexPatternUtils.java @@ -0,0 +1,524 @@ +package stirling.software.common.util; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public final class RegexPatternUtils { + + private static final RegexPatternUtils INSTANCE = new RegexPatternUtils(); + private final ConcurrentHashMap patternCache = new ConcurrentHashMap<>(); + + private static final String WHITESPACE_REGEX = "\\s++"; + private static final String EXTENSION_REGEX = "\\.(?:[^.]*+)?$"; + + private RegexPatternUtils() { + super(); + // Initialize with commonly used patterns for immediate availability + precompileCommonPatterns(); + } + + /** + * Get the singleton instance of the pattern cache. + * + * @return the singleton RegexPatternCache instance + */ + public static RegexPatternUtils getInstance() { + return INSTANCE; + } + + /** + * Get a compiled pattern from cache, compiling and caching if not present. + * + *

This method is thread-safe and uses lazy initialization. Multiple threads calling with the + * same regex will result in only one compilation, with all threads receiving the same cached + * Pattern instance. + * + *

Performance: first call compiles and caches (expensive), subsequent calls return cached + * pattern (fast O(1) lookup). + * + * @param regex the regular expression string to compile + * @return compiled Pattern object, never null + * @throws PatternSyntaxException if the regex syntax is invalid + * @throws IllegalArgumentException if regex is null + */ + public Pattern getPattern(String regex) { + if (regex == null) { + throw new IllegalArgumentException("Regex pattern cannot be null"); + } + + return patternCache.computeIfAbsent(new PatternKey(regex, 0), this::compilePattern); + } + + /** + * Get a compiled pattern with flags. + * + *

Patterns with different flags are cached separately using a composite key. Common flags + * include: + * + *

    + *
  • {@link Pattern#CASE_INSENSITIVE} - ignore case differences + *
  • {@link Pattern#MULTILINE} - ^ and $ match line boundaries + *
  • {@link Pattern#DOTALL} - . matches any character including newlines + *
+ * + * @param regex the regular expression string + * @param flags pattern flags (e.g., Pattern.CASE_INSENSITIVE) + * @return compiled Pattern object with specified flags + * @throws PatternSyntaxException if the regex syntax is invalid + * @throws IllegalArgumentException if regex is null + */ + public Pattern getPattern(String regex, int flags) { + if (regex == null) { + throw new IllegalArgumentException("Regex pattern cannot be null"); + } + + return patternCache.computeIfAbsent(new PatternKey(regex, flags), this::compilePattern); + } + + /** + * Check if a pattern is already cached. + * + * @param regex the regular expression string + * @return true if pattern is cached, false otherwise + */ + public boolean isCached(String regex) { + return isCached(regex, 0); + } + + /** + * Check if a pattern with flags is already cached. + * + * @param regex the regular expression string + * @param flags pattern flags + * @return true if pattern is cached, false otherwise + */ + public boolean isCached(String regex, int flags) { + return regex != null && patternCache.containsKey(new PatternKey(regex, flags)); + } + + /** + * Get current cache size (number of cached patterns). Useful for monitoring and debugging. + * + * @return number of patterns currently cached + */ + public int getCacheSize() { + return patternCache.size(); + } + + /** + * Clear all cached patterns. Use sparingly as it forces recompilation of all patterns. Mainly + * useful for testing or memory cleanup in long-running applications. + */ + public void clearCache() { + patternCache.clear(); + log.debug("Regex pattern cache cleared"); + } + + /** + * Remove a specific pattern from cache. + * + * @param regex the regular expression string to remove + * @return true if pattern was cached and removed, false otherwise + */ + public boolean removeFromCache(String regex) { + return removeFromCache(regex, 0); + } + + /** + * Remove a specific pattern with flags from cache. + * + * @param regex the regular expression string to remove + * @param flags pattern flags + * @return true if pattern was cached and removed, false otherwise + */ + public boolean removeFromCache(String regex, int flags) { + if (regex == null) { + return false; + } + PatternKey key = new PatternKey(regex, flags); + boolean removed = patternCache.remove(key) != null; + if (removed) { + log.debug("Removed regex pattern from cache: {} (flags: {})", regex, flags); + } + return removed; + } + + /** + * Internal method to compile a pattern and handle errors consistently. + * + * @return compiled Pattern + * @throws PatternSyntaxException if regex is invalid + */ + private Pattern compilePattern(PatternKey key) { + String regex = key.regex; + int flags = key.flags; + + try { + Pattern pattern = Pattern.compile(regex, flags); + log.trace("Compiled and cached regex pattern with flags {}: {}", flags, regex); + return pattern; + } catch (PatternSyntaxException e) { + log.error( + "Invalid regex pattern: '{}' with flags {} - {}", regex, flags, e.getMessage()); + throw e; + } + } + + public static String getWhitespaceRegex() { + return WHITESPACE_REGEX; + } + + /** Creates a case-insensitive pattern for text searching */ + public Pattern createSearchPattern(String regex, boolean caseInsensitive) { + int flags = caseInsensitive ? (Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE) : 0; + return getPattern(regex, flags); + } + + /** Pattern for matching trailing slashes (e.g., "/path/to/dir///") */ + public Pattern getTrailingSlashesPattern() { + return getPattern("/+$"); + } + + /** Pattern for removing drive letters from paths */ + public Pattern getDriveLetterPattern() { + return getPattern("^[a-zA-Z]:[\\\\/]+"); + } + + /** Pattern for removing leading slashes from paths */ + public Pattern getLeadingSlashesPattern() { + return getPattern("^[\\\\/]+"); + } + + /** Pattern for matching backslashes */ + public Pattern getBackslashPattern() { + return getPattern("\\\\"); + } + + /** Pattern for sanitizing filenames by removing problematic characters */ + public Pattern getSafeFilenamePattern() { + return getPattern("[/\\\\?%*:|\"<>]"); + } + + /** Pattern for sanitizing filenames (keeps only alphanumeric) */ + public Pattern getFilenameSafePattern() { + return getPattern("[^a-zA-Z0-9]"); + } + + /** + * Pattern for replacing non-alphanumeric characters with underscore (explicit underscore + * variant) + */ + public Pattern getNonAlnumUnderscorePattern() { + return getPattern("[^A-Za-z0-9_]"); + } + + /** Pattern for collapsing multiple underscores */ + public Pattern getMultipleUnderscoresPattern() { + return getPattern("_+"); + } + + /** Pattern for trimming leading underscores */ + public Pattern getLeadingUnderscoresPattern() { + return getPattern("^_+"); + } + + /** Pattern for trimming trailing underscores */ + public Pattern getTrailingUnderscoresPattern() { + return getPattern("_+$"); + } + + /** Pattern for matching upload/download paths (case insensitive) */ + public Pattern getUploadDownloadPathPattern() { + return getPattern("(?i).*/(upload|download)/.*"); + } + + /** Pattern for matching one or more whitespace characters */ + public Pattern getWhitespacePattern() { + return getPattern("\\s+"); + } + + /** Pattern for matching newlines (Windows and Unix style) */ + public Pattern getNewlinesPattern() { + return getPattern("\\r?\\n"); + } + + /** Pattern for splitting on newlines (Windows and Unix style) */ + public Pattern getNewlineSplitPattern() { + return getPattern("\\r?\\n"); + } + + /** Pattern for splitting text into words */ + public Pattern getWordSplitPattern() { + return getPattern("\\s+"); + } + + /** Pattern for removing carriage returns */ + public Pattern getCarriageReturnPattern() { + return getPattern("\\r"); + } + + /** Pattern for matching newline characters */ + public Pattern getNewlineCharsPattern() { + return getPattern("[\n\r]"); + } + + /** Pattern for multi-format newline splitting (Windows, Mac, Unix) */ + public Pattern getMultiFormatNewlinePattern() { + return getPattern("\r\n|\r|\n"); + } + + /** Pattern for encoded payload newline removal */ + public Pattern getEncodedPayloadNewlinePattern() { + return getPattern("\\r?\\n"); + } + + /** Pattern for escaped newlines in watermark text */ + public Pattern getEscapedNewlinePattern() { + return getPattern("\\\\n"); + } + + /** Pattern for input sanitization (allows only alphanumeric and spaces) */ + public Pattern getInputSanitizePattern() { + return getPattern("[^a-zA-Z0-9 ]"); + } + + /** Pattern for removing angle brackets */ + public Pattern getAngleBracketsPattern() { + return getPattern("[<>]"); + } + + /** Pattern for removing leading and trailing quotes */ + public Pattern getQuotesRemovalPattern() { + return getPattern("^\"|\"$"); + } + + /** Pattern for plus signs (URL encoding replacement) */ + public Pattern getPlusSignPattern() { + return getPattern("\\+"); + } + + /** Pattern for username validation */ + public Pattern getUsernameValidationPattern() { + return getPattern("^[a-zA-Z0-9](?!.*[-@._+]{2,})[a-zA-Z0-9@._+-]{1,48}[a-zA-Z0-9]$"); + } + + public static String getExtensionRegex() { + return EXTENSION_REGEX; + } + + /** Pattern for extracting non-numeric characters */ + public Pattern getNumericExtractionPattern() { + return getPattern("\\D"); + } + + /** Pattern for removing non-digit/dot characters (for timeout parsing) */ + public Pattern getNonDigitDotPattern() { + return getPattern("[^\\d.]"); + } + + /** Pattern for matching digit/dot characters (for timeout parsing) */ + public Pattern getDigitDotPattern() { + return getPattern("[\\d.]"); + } + + /** Pattern for detecting strings containing digits */ + public Pattern getContainsDigitsPattern() { + return getPattern(".*\\d+.*"); + } + + /** Pattern for matching 1-3 digit numbers */ + public Pattern getNumberRangePattern() { + return getPattern("[1-9][0-9]{0,2}"); + } + + /** Pattern for validating mathematical expressions */ + public Pattern getMathExpressionPattern() { + return getPattern("[0-9n+\\-*/() ]+"); + } + + /** Pattern for adding multiplication between numbers and 'n' */ + public Pattern getNumberBeforeNPattern() { + return getPattern("(\\d)n"); + } + + /** Pattern for detecting consecutive 'n' characters */ + public Pattern getConsecutiveNPattern() { + return getPattern(".*n{2,}.*"); + } + + /** Pattern for replacing consecutive 'n' characters */ + public Pattern getConsecutiveNReplacementPattern() { + return getPattern("(?]*>.*?"); + } + + /** Pattern for removing style tags from HTML */ + public Pattern getStyleTagPattern() { + return getPattern("(?i)]*>.*?"); + } + + /** Pattern for removing fixed position CSS */ + public Pattern getFixedPositionCssPattern() { + return getPattern("(?i)\\s*position\\s*:\\s*fixed[^;]*;?"); + } + + /** Pattern for removing absolute position CSS */ + public Pattern getAbsolutePositionCssPattern() { + return getPattern("(?i)\\s*position\\s*:\\s*absolute[^;]*;?"); + } + + /** Pattern for matching size unit suffixes (KB, MB, GB, etc.) */ + public Pattern getSizeUnitPattern() { + return getPattern("[KMGkmg][Bb]"); + } + + /** Pattern for system temp file type 1 */ + public Pattern getSystemTempFile1Pattern() { + return getPattern("lu\\d+[a-z0-9]*\\.tmp"); + } + + /** Pattern for system temp file type 2 (OCR processes) */ + public Pattern getSystemTempFile2Pattern() { + return getPattern("ocr_process\\d+"); + } + + /** Pattern for splitting on whitespace and parentheses */ + public Pattern getWhitespaceParenthesesSplitPattern() { + return getPattern("[\\s\\(\\)]+"); + } + + /** Pattern for MIME header whitespace cleanup before encoded sequences */ + public Pattern getMimeHeaderWhitespacePattern() { + return getPattern("\\s+(?==\\?)"); + } + + /** Pattern for font name validation (6 uppercase letters + plus + rest) */ + public Pattern getFontNamePattern() { + return getPattern("^[A-Z]{6}\\+.*"); + } + + /** Pattern for matching access="readOnly" attribute in XFA XML (with optional whitespace) */ + public Pattern getAccessReadOnlyPattern() { + return getPattern("access\\s*=\\s*\"readOnly\""); + } + + /** Pattern for matching MIME encoded-word headers (RFC 2047) Example: =?charset?B?encoded?= */ + public Pattern getMimeEncodedWordPattern() { + return getPattern("=\\?([^?]+)\\?([BbQq])\\?([^?]*)\\?="); + } + + /** Pattern for matching inline CID images in HTML (case-insensitive) */ + public Pattern getInlineCidImagePattern() { + return getPattern( + "(?i)]*\\ssrc\\s*=\\s*['\"]cid:([^'\"]+)['\"][^>]*>", + Pattern.CASE_INSENSITIVE); + } + + /** Pattern for matching image file extensions (case-insensitive) */ + public Pattern getImageFilePattern() { + return getPattern(".*\\.(jpg|jpeg|png|gif|bmp|webp)$", Pattern.CASE_INSENSITIVE); + } + + /** Pattern for matching attachment section headers (case-insensitive) */ + public Pattern getAttachmentSectionPattern() { + return getPattern("attachments\\s*\\(\\d+\\)", Pattern.CASE_INSENSITIVE); + } + + /** Pattern for matching filenames in attachment markers */ + public Pattern getAttachmentFilenamePattern() { + return getPattern("@\\s*([^\\s\\(]+(?:\\.[a-zA-Z0-9]+)?)"); + } + + /** Pattern for matching pdfaid:part attribute in XMP metadata */ + public Pattern getPdfAidPartPattern() { + return getPattern("pdfaid:part[\"\\s]*=[\"\\s]*([0-9]+)"); + } + + /** Pattern for matching pdfaid:conformance attribute in XMP metadata */ + public Pattern getPdfAidConformancePattern() { + return getPattern("pdfaid:conformance[\"\\s]*=[\"\\s]*([A-Za-z]+)"); + } + + /** Pattern for matching slash in page mode description */ + public Pattern getPageModePattern() { + return getPattern("/"); + } + + /** + * Pre-compile commonly used patterns for immediate availability. This eliminates first-call + * compilation overhead for frequent patterns. + */ + private void precompileCommonPatterns() { + getPattern("\\.(?:[^.]*+)?$"); // Extension removal - possessive, optional, anchored + getPattern("\\.[^.]+$"); // Simple extension match - anchored + + getPattern("\\s+"); // One or more whitespace + getPattern("\\s*"); // Zero or more whitespace + + getPattern("/+$"); // Trailing slashes + getPattern("\\D"); // Non-numeric characters + getPattern("[/\\\\?%*:|\"<>]"); // Unsafe filename characters + getPattern("[^a-zA-Z0-9 ]"); // Input sanitization + getPattern("[^a-zA-Z0-9]"); // Filename sanitization + // API doc patterns + getPattern("Output:(\\w+)"); // precompiled single-escaped for runtime regex \w + getPattern("Input:(\\w+)"); + getPattern("Type:(\\w+)"); + log.debug("Pre-compiled {} common regex patterns", patternCache.size()); + } + + /** Pattern for email validation */ + public Pattern getEmailValidationPattern() { + return getPattern( + "^(?=.{1,320}$)(?=.{1,64}@)[A-Za-z0-9](?:[A-Za-z0-9_.+-]*[A-Za-z0-9])?@[^-][A-Za-z0-9-]+(?:\\.[A-Za-z0-9-]+)*(?:\\.[A-Za-z]{2,})$"); + } + + /* Pattern for matching Output: in API descriptions */ + public Pattern getApiDocOutputTypePattern() { + return getPattern("Output:(\\w+)"); + } + + /* Pattern for matching Input: in API descriptions */ + public Pattern getApiDocInputTypePattern() { + return getPattern("Input:(\\w+)"); + } + + /** + * Pattern for matching Type: in API descriptions + */ + public Pattern getApiDocTypePattern() { + return getPattern("Type:(\\w+)"); + } + + /* Pattern for validating file extensions (2-4 alphanumeric, case-insensitive) */ + public Pattern getFileExtensionValidationPattern() { + return getPattern("^[a-zA-Z0-9]{2,4}$", Pattern.CASE_INSENSITIVE); + } + + private record PatternKey(String regex, int flags) { + // Record automatically provides equals, hashCode, and toString + } +} diff --git a/app/common/src/main/java/stirling/software/common/util/TempFile.java b/app/common/src/main/java/stirling/software/common/util/TempFile.java index db859c431..2c730c937 100644 --- a/app/common/src/main/java/stirling/software/common/util/TempFile.java +++ b/app/common/src/main/java/stirling/software/common/util/TempFile.java @@ -4,6 +4,7 @@ import java.io.File; import java.io.IOException; import java.nio.file.Path; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; /** @@ -14,17 +15,13 @@ import lombok.extern.slf4j.Slf4j; public class TempFile implements AutoCloseable { private final TempFileManager manager; - private final File file; + @Getter private final File file; public TempFile(TempFileManager manager, String suffix) throws IOException { this.manager = manager; this.file = manager.createTempFile(suffix); } - public File getFile() { - return file; - } - public Path getPath() { return file.toPath(); } diff --git a/app/common/src/main/java/stirling/software/common/util/TempFileRegistry.java b/app/common/src/main/java/stirling/software/common/util/TempFileRegistry.java index 323b3bff3..ab1304f5e 100644 --- a/app/common/src/main/java/stirling/software/common/util/TempFileRegistry.java +++ b/app/common/src/main/java/stirling/software/common/util/TempFileRegistry.java @@ -13,6 +13,7 @@ import java.util.stream.Collectors; import org.springframework.stereotype.Component; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; /** @@ -24,8 +25,22 @@ import lombok.extern.slf4j.Slf4j; public class TempFileRegistry { private final ConcurrentMap registeredFiles = new ConcurrentHashMap<>(); + + /** + * -- GETTER -- Get all registered third-party temporary files. + * + * @return Set of third-party file paths + */ + @Getter private final Set thirdPartyTempFiles = Collections.newSetFromMap(new ConcurrentHashMap<>()); + + /** + * -- GETTER -- Get all registered temporary directories. + * + * @return Set of temporary directory paths + */ + @Getter private final Set tempDirectories = Collections.newSetFromMap(new ConcurrentHashMap<>()); /** @@ -133,24 +148,6 @@ public class TempFileRegistry { .collect(Collectors.toSet()); } - /** - * Get all registered third-party temporary files. - * - * @return Set of third-party file paths - */ - public Set getThirdPartyTempFiles() { - return thirdPartyTempFiles; - } - - /** - * Get all registered temporary directories. - * - * @return Set of temporary directory paths - */ - public Set getTempDirectories() { - return tempDirectories; - } - /** * Check if a file is registered in the registry. * diff --git a/app/common/src/main/java/stirling/software/common/util/WebResponseUtils.java b/app/common/src/main/java/stirling/software/common/util/WebResponseUtils.java index 745f5d5ec..3ab0bddb3 100644 --- a/app/common/src/main/java/stirling/software/common/util/WebResponseUtils.java +++ b/app/common/src/main/java/stirling/software/common/util/WebResponseUtils.java @@ -4,6 +4,8 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import org.apache.pdfbox.pdmodel.PDDocument; import org.springframework.http.HttpHeaders; @@ -11,9 +13,13 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.github.pixee.security.Filenames; +import lombok.extern.slf4j.Slf4j; + +@Slf4j public class WebResponseUtils { public static ResponseEntity baosToWebResponse( @@ -44,7 +50,10 @@ public class WebResponseUtils { headers.setContentType(mediaType); headers.setContentLength(bytes.length); String encodedDocName = - URLEncoder.encode(docName, StandardCharsets.UTF_8).replaceAll("\\+", "%20"); + RegexPatternUtils.getInstance() + .getPlusSignPattern() + .matcher(URLEncoder.encode(docName, StandardCharsets.UTF_8)) + .replaceAll("%20"); headers.setContentDispositionFormData("attachment", encodedDocName); return new ResponseEntity<>(bytes, headers, HttpStatus.OK); } @@ -64,4 +73,59 @@ public class WebResponseUtils { return baosToWebResponse(baos, docName); } + + /** + * Convert a File to a web response (PDF default). + * + * @param outputTempFile The temporary file to be sent as a response. + * @param docName The name of the document. + * @return A ResponseEntity containing the file as a resource. + */ + public static ResponseEntity pdfFileToWebResponse( + TempFile outputTempFile, String docName) throws IOException { + return fileToWebResponse(outputTempFile, docName, MediaType.APPLICATION_PDF); + } + + /** + * Convert a File to a web response (ZIP default). + * + * @param outputTempFile The temporary file to be sent as a response. + * @param docName The name of the document. + * @return A ResponseEntity containing the file as a resource. + */ + public static ResponseEntity zipFileToWebResponse( + TempFile outputTempFile, String docName) throws IOException { + return fileToWebResponse(outputTempFile, docName, MediaType.APPLICATION_OCTET_STREAM); + } + + /** + * Convert a File to a web response with explicit media type (e.g., ZIP). + * + * @param outputTempFile The temporary file to be sent as a response. + * @param docName The name of the document. + * @param mediaType The content type to set on the response. + * @return A ResponseEntity containing the file as a resource. + */ + public static ResponseEntity fileToWebResponse( + TempFile outputTempFile, String docName, MediaType mediaType) throws IOException { + + Path path = outputTempFile.getFile().toPath().normalize(); + long len = Files.size(path); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(mediaType); + headers.setContentLength(len); + headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + docName + "\""); + + StreamingResponseBody body = + os -> { + try (os) { + Files.copy(path, os); + os.flush(); + } finally { + outputTempFile.close(); + } + }; + + return new ResponseEntity<>(body, headers, HttpStatus.OK); + } } diff --git a/app/common/src/main/java/stirling/software/common/util/misc/ColorSpaceConversionStrategy.java b/app/common/src/main/java/stirling/software/common/util/misc/ColorSpaceConversionStrategy.java new file mode 100644 index 000000000..ca4970b71 --- /dev/null +++ b/app/common/src/main/java/stirling/software/common/util/misc/ColorSpaceConversionStrategy.java @@ -0,0 +1,86 @@ +package stirling.software.common.util.misc; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.core.io.InputStreamResource; +import org.springframework.web.multipart.MultipartFile; + +import lombok.extern.slf4j.Slf4j; + +import stirling.software.common.model.api.misc.ReplaceAndInvert; +import stirling.software.common.util.ProcessExecutor; +import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; + +@Slf4j +public class ColorSpaceConversionStrategy extends ReplaceAndInvertColorStrategy { + + private final TempFileManager tempFileManager; + + public ColorSpaceConversionStrategy( + MultipartFile file, + ReplaceAndInvert replaceAndInvert, + TempFileManager tempFileManager) { + super(file, replaceAndInvert); + this.tempFileManager = tempFileManager; + } + + @Override + public InputStreamResource replace() throws IOException { + try (TempFile tempInput = new TempFile(tempFileManager, ".pdf"); + TempFile tempOutput = new TempFile(tempFileManager, ".pdf")) { + + Path tempInputFile = tempInput.getPath(); + Path tempOutputFile = tempOutput.getPath(); + + Files.write(tempInputFile, getFileInput().getBytes()); + + log.info("Starting CMYK color space conversion"); + + List command = new ArrayList<>(); + command.add("gs"); + command.add("-sDEVICE=pdfwrite"); + command.add("-dCompatibilityLevel=1.5"); + command.add("-dPDFSETTINGS=/prepress"); + command.add("-dNOPAUSE"); + command.add("-dQUIET"); + command.add("-dBATCH"); + command.add("-sProcessColorModel=DeviceCMYK"); + command.add("-sColorConversionStrategy=CMYK"); + command.add("-sColorConversionStrategyForImages=CMYK"); + command.add("-sOutputFile=" + tempOutputFile.toString()); + command.add(tempInputFile.toString()); + + log.debug("Executing Ghostscript command for CMYK conversion: {}", command); + + ProcessExecutorResult result = + ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT) + .runCommandWithOutputHandling(command); + + if (result.getRc() != 0) { + log.error( + "Ghostscript CMYK conversion failed with return code: {}. Output: {}", + result.getRc(), + result.getMessages()); + throw new IOException( + "CMYK color space conversion failed: " + result.getMessages()); + } + + log.info("CMYK color space conversion completed successfully"); + + byte[] pdfBytes = Files.readAllBytes(tempOutputFile); + return new InputStreamResource(new ByteArrayInputStream(pdfBytes)); + + } catch (Exception e) { + log.warn("CMYK color space conversion failed", e); + throw new IOException( + "Failed to convert PDF to CMYK color space: " + e.getMessage(), e); + } + } +} diff --git a/app/common/src/main/java/stirling/software/common/util/misc/InvertFullColorStrategy.java b/app/common/src/main/java/stirling/software/common/util/misc/InvertFullColorStrategy.java index df40737d3..d220cfcfa 100644 --- a/app/common/src/main/java/stirling/software/common/util/misc/InvertFullColorStrategy.java +++ b/app/common/src/main/java/stirling/software/common/util/misc/InvertFullColorStrategy.java @@ -19,7 +19,10 @@ import org.apache.pdfbox.rendering.PDFRenderer; import org.springframework.core.io.InputStreamResource; import org.springframework.web.multipart.MultipartFile; +import stirling.software.common.model.ApplicationProperties; import stirling.software.common.model.api.misc.ReplaceAndInvert; +import stirling.software.common.util.ApplicationContextProvider; +import stirling.software.common.util.ExceptionUtils; public class InvertFullColorStrategy extends ReplaceAndInvertColorStrategy { @@ -44,8 +47,25 @@ public class InvertFullColorStrategy extends ReplaceAndInvertColorStrategy { // Render each page and invert colors PDFRenderer pdfRenderer = new PDFRenderer(document); for (int page = 0; page < document.getNumberOfPages(); page++) { - BufferedImage image = - pdfRenderer.renderImageWithDPI(page, 300); // Render page at 300 DPI + BufferedImage image; + + // Use global maximum DPI setting, fallback to 300 if not set + int renderDpi = 300; // Default fallback + ApplicationProperties properties = + ApplicationContextProvider.getBean(ApplicationProperties.class); + if (properties != null && properties.getSystem() != null) { + renderDpi = properties.getSystem().getMaxDPI(); + } + + try { + image = + pdfRenderer.renderImageWithDPI( + page, renderDpi); // Render page with global DPI setting + } catch (OutOfMemoryError e) { + throw ExceptionUtils.createOutOfMemoryDpiException(page + 1, renderDpi, e); + } catch (NegativeArraySizeException e) { + throw ExceptionUtils.createOutOfMemoryDpiException(page + 1, renderDpi, e); + } // Invert the colors invertImageColors(image); diff --git a/app/common/src/test/java/stirling/software/common/annotations/AutoJobPostMappingIntegrationTest.java b/app/common/src/test/java/stirling/software/common/annotations/AutoJobPostMappingIntegrationTest.java index 2c4546ac0..dd7a0f79b 100644 --- a/app/common/src/test/java/stirling/software/common/annotations/AutoJobPostMappingIntegrationTest.java +++ b/app/common/src/test/java/stirling/software/common/annotations/AutoJobPostMappingIntegrationTest.java @@ -29,7 +29,6 @@ import jakarta.servlet.http.HttpServletRequest; import stirling.software.common.aop.AutoJobAspect; import stirling.software.common.model.api.PDFFile; -import stirling.software.common.service.FileOrUploadService; import stirling.software.common.service.FileStorage; import stirling.software.common.service.JobExecutorService; import stirling.software.common.service.JobQueue; @@ -44,8 +43,6 @@ class AutoJobPostMappingIntegrationTest { @Mock private HttpServletRequest request; - @Mock private FileOrUploadService fileOrUploadService; - @Mock private FileStorage fileStorage; @Mock private ResourceMonitor resourceMonitor; @@ -54,8 +51,7 @@ class AutoJobPostMappingIntegrationTest { @BeforeEach void setUp() { - autoJobAspect = - new AutoJobAspect(jobExecutorService, request, fileOrUploadService, fileStorage); + autoJobAspect = new AutoJobAspect(jobExecutorService, request, fileStorage); } @Mock private ProceedingJoinPoint joinPoint; diff --git a/app/common/src/test/java/stirling/software/common/model/ApplicationPropertiesLogicTest.java b/app/common/src/test/java/stirling/software/common/model/ApplicationPropertiesLogicTest.java index ba98bf0e1..f075f1518 100644 --- a/app/common/src/test/java/stirling/software/common/model/ApplicationPropertiesLogicTest.java +++ b/app/common/src/test/java/stirling/software/common/model/ApplicationPropertiesLogicTest.java @@ -2,9 +2,11 @@ package stirling.software.common.model; import static org.junit.jupiter.api.Assertions.*; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.function.Function; import org.junit.jupiter.api.Test; @@ -31,22 +33,23 @@ class ApplicationPropertiesLogicTest { @Test void tempFileManagement_defaults_and_overrides() { + Function normalize = s -> Paths.get(s).normalize().toString(); ApplicationProperties.TempFileManagement tfm = new ApplicationProperties.TempFileManagement(); String expectedBase = - java.lang.System.getProperty("java.io.tmpdir").replaceAll("/+$", "") - + "/stirling-pdf"; + Paths.get(java.lang.System.getProperty("java.io.tmpdir"), "stirling-pdf") + .toString(); assertEquals(expectedBase, tfm.getBaseTmpDir()); - String expectedLibre = expectedBase + "/libreoffice"; + String expectedLibre = Paths.get(expectedBase, "libreoffice").toString(); assertEquals(expectedLibre, tfm.getLibreofficeDir()); tfm.setBaseTmpDir("/custom/base"); - assertEquals("/custom/base", tfm.getBaseTmpDir()); + assertEquals(normalize.apply("/custom/base"), normalize.apply(tfm.getBaseTmpDir())); tfm.setLibreofficeDir("/opt/libre"); - assertEquals("/opt/libre", tfm.getLibreofficeDir()); + assertEquals(normalize.apply("/opt/libre"), normalize.apply(tfm.getLibreofficeDir())); } @Test @@ -155,7 +158,7 @@ class ApplicationPropertiesLogicTest { assertEquals(30, t.getOcrMyPdfTimeoutMinutes()); } - @Deprecated + @Deprecated(since = "0.45.0") @Test void enterprise_metadata_defaults() { ApplicationProperties.EnterpriseEdition ee = new ApplicationProperties.EnterpriseEdition(); diff --git a/app/common/src/test/java/stirling/software/common/model/ApplicationPropertiesSaml2HttpTest.java b/app/common/src/test/java/stirling/software/common/model/ApplicationPropertiesSaml2HttpTest.java index 3fa8299ca..0e3545e8b 100644 --- a/app/common/src/test/java/stirling/software/common/model/ApplicationPropertiesSaml2HttpTest.java +++ b/app/common/src/test/java/stirling/software/common/model/ApplicationPropertiesSaml2HttpTest.java @@ -11,6 +11,7 @@ import java.nio.file.Path; import org.junit.jupiter.api.Test; import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; +import org.springframework.http.MediaType; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; @@ -23,7 +24,7 @@ class ApplicationPropertiesSaml2HttpTest { server.enqueue( new MockResponse() .setResponseCode(200) - .addHeader("Content-Type", "application/xml") + .addHeader("Content-Type", MediaType.APPLICATION_XML_VALUE) .setBody("")); server.start(); diff --git a/app/common/src/test/java/stirling/software/common/model/FileInfoTest.java b/app/common/src/test/java/stirling/software/common/model/FileInfoTest.java new file mode 100644 index 000000000..06f30c9fd --- /dev/null +++ b/app/common/src/test/java/stirling/software/common/model/FileInfoTest.java @@ -0,0 +1,111 @@ +package stirling.software.common.model; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.File; +import java.time.LocalDateTime; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +public class FileInfoTest { + + @ParameterizedTest(name = "{index}: fileSize={0}") + @CsvSource({ + "0, '0 Bytes'", + "1023, '1023 Bytes'", + "1024, '1.00 KB'", + "1048575, '1024.00 KB'", // Do we really want this as result? + "1048576, '1.00 MB'", + "1073741823, '1024.00 MB'", // Do we really want this as result? + "1073741824, '1.00 GB'" + }) + void testGetFormattedFileSize(long fileSize, String expectedFormattedSize) { + FileInfo fileInfo = + new FileInfo( + "example.txt", + File.separator + + "path" + + File.separator + + "to" + + File.separator + + "example.txt", + LocalDateTime.now(), + fileSize, + LocalDateTime.now().minusDays(1)); + + assertEquals(expectedFormattedSize, fileInfo.getFormattedFileSize()); + } + + @Test + void testGetFilePathAsPath() { + FileInfo fileInfo = + new FileInfo( + "test.pdf", + File.separator + "tmp" + File.separator + "test.pdf", + LocalDateTime.now(), + 1234, + LocalDateTime.now().minusDays(2)); + assertEquals( + File.separator + "tmp" + File.separator + "test.pdf", + fileInfo.getFilePathAsPath().toString()); + } + + @Test + void testGetFormattedModificationDate() { + LocalDateTime modDate = LocalDateTime.of(2024, 6, 1, 15, 30, 45); + FileInfo fileInfo = + new FileInfo( + "file.txt", + File.separator + "file.txt", + modDate, + 100, + LocalDateTime.of(2024, 5, 31, 10, 0, 0)); + assertEquals("2024-06-01 15:30:45", fileInfo.getFormattedModificationDate()); + } + + @Test + void testGetFormattedCreationDate() { + LocalDateTime creationDate = LocalDateTime.of(2023, 12, 25, 8, 15, 0); + FileInfo fileInfo = + new FileInfo( + "holiday.txt", + File.separator + "holiday.txt", + LocalDateTime.of(2024, 1, 1, 0, 0, 0), + 500, + creationDate); + assertEquals("2023-12-25 08:15:00", fileInfo.getFormattedCreationDate()); + } + + @Test + void testGettersAndSetters() { + LocalDateTime now = LocalDateTime.now(); + FileInfo fileInfo = + new FileInfo( + "doc.pdf", + File.separator + "docs" + File.separator + "doc.pdf", + now, + 2048, + now.minusDays(1)); + // Test getters + assertEquals("doc.pdf", fileInfo.getFileName()); + assertEquals(File.separator + "docs" + File.separator + "doc.pdf", fileInfo.getFilePath()); + assertEquals(now, fileInfo.getModificationDate()); + assertEquals(2048, fileInfo.getFileSize()); + assertEquals(now.minusDays(1), fileInfo.getCreationDate()); + + // Test setters + fileInfo.setFileName("new.pdf"); + fileInfo.setFilePath(File.separator + "new" + File.separator + "new.pdf"); + fileInfo.setModificationDate(now.plusDays(1)); + fileInfo.setFileSize(4096); + fileInfo.setCreationDate(now.minusDays(2)); + + assertEquals("new.pdf", fileInfo.getFileName()); + assertEquals(File.separator + "new" + File.separator + "new.pdf", fileInfo.getFilePath()); + assertEquals(now.plusDays(1), fileInfo.getModificationDate()); + assertEquals(4096, fileInfo.getFileSize()); + assertEquals(now.minusDays(2), fileInfo.getCreationDate()); + } +} diff --git a/app/common/src/test/java/stirling/software/common/model/InputStreamTemplateResourceTest.java b/app/common/src/test/java/stirling/software/common/model/InputStreamTemplateResourceTest.java new file mode 100644 index 000000000..6543c1763 --- /dev/null +++ b/app/common/src/test/java/stirling/software/common/model/InputStreamTemplateResourceTest.java @@ -0,0 +1,88 @@ +package stirling.software.common.model; + +/* Commented out - InputStreamTemplateResource class removed with Thymeleaf migration + * This test will be removed when frontend migration to React is complete + + +public class InputStreamTemplateResourceTest { + + @Test + void gettersReturnProvidedFields() { + byte[] data = {1, 2, 3}; + InputStream is = new ByteArrayInputStream(data); + String encoding = "UTF-8"; + InputStreamTemplateResource resource = new InputStreamTemplateResource(is, encoding); + + assertSame(is, resource.getInputStream()); + assertEquals(encoding, resource.getCharacterEncoding()); + } + + @Test + void fieldsAreFinal() throws NoSuchFieldException { + Field inputStreamField = InputStreamTemplateResource.class.getDeclaredField("inputStream"); + Field characterEncodingField = + InputStreamTemplateResource.class.getDeclaredField("characterEncoding"); + + assertTrue(Modifier.isFinal(inputStreamField.getModifiers())); + assertTrue(Modifier.isFinal(characterEncodingField.getModifiers())); + } + + @Test + void noSetterMethodsPresent() { + long setterCount = + Arrays.stream(InputStreamTemplateResource.class.getDeclaredMethods()) + .filter(method -> method.getName().startsWith("set")) + .count(); + + assertEquals(0, setterCount, "InputStreamTemplateResource should not have setter methods"); + } + + @Test + void readerReturnsCorrectContent() throws Exception { + String content = "Hello, world!"; + InputStream is = new ByteArrayInputStream(content.getBytes("UTF-8")); + InputStreamTemplateResource resource = new InputStreamTemplateResource(is, "UTF-8"); + + try (Reader reader = resource.reader()) { + char[] buffer = new char[content.length()]; + int read = reader.read(buffer); + assertEquals(content.length(), read); + assertEquals(content, new String(buffer)); + } + } + + @Test + void relativeThrowsUnsupportedOperationException() { + InputStream is = new ByteArrayInputStream(new byte[0]); + InputStreamTemplateResource resource = new InputStreamTemplateResource(is, "UTF-8"); + assertThrows(UnsupportedOperationException.class, () -> resource.relative("other")); + } + + @Test + void getDescriptionReturnsExpectedString() { + InputStream is = new ByteArrayInputStream(new byte[0]); + InputStreamTemplateResource resource = new InputStreamTemplateResource(is, "UTF-8"); + assertEquals("InputStream resource [Stream]", resource.getDescription()); + } + + @Test + void getBaseNameReturnsExpectedString() { + InputStream is = new ByteArrayInputStream(new byte[0]); + InputStreamTemplateResource resource = new InputStreamTemplateResource(is, "UTF-8"); + assertEquals("streamResource", resource.getBaseName()); + } + + @Test + void existsReturnsTrueWhenInputStreamNotNull() { + InputStream is = new ByteArrayInputStream(new byte[0]); + InputStreamTemplateResource resource = new InputStreamTemplateResource(is, "UTF-8"); + assertTrue(resource.exists()); + } + + @Test + void existsReturnsFalseWhenInputStreamIsNull() { + InputStreamTemplateResource resource = new InputStreamTemplateResource(null, "UTF-8"); + assertFalse(resource.exists()); + } +} +*/ diff --git a/app/common/src/test/java/stirling/software/common/service/CustomPDFDocumentFactoryTest.java b/app/common/src/test/java/stirling/software/common/service/CustomPDFDocumentFactoryTest.java index f0b2ae3a4..50c1bc959 100644 --- a/app/common/src/test/java/stirling/software/common/service/CustomPDFDocumentFactoryTest.java +++ b/app/common/src/test/java/stirling/software/common/service/CustomPDFDocumentFactoryTest.java @@ -1,12 +1,12 @@ package stirling.software.common.service; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; import static stirling.software.common.service.SpyPDFDocumentFactory.*; import java.io.*; import java.nio.file.*; -import java.nio.file.Files; import java.util.Arrays; import org.apache.pdfbox.Loader; @@ -18,9 +18,11 @@ import org.junit.jupiter.api.parallel.Execution; import org.junit.jupiter.api.parallel.ExecutionMode; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; +import org.springframework.http.MediaType; import org.springframework.mock.web.MockMultipartFile; import stirling.software.common.model.api.PDFFile; +import stirling.software.common.service.SpyPDFDocumentFactory.StrategyType; @TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(MethodOrderer.OrderAnnotation.class) @@ -73,7 +75,7 @@ class CustomPDFDocumentFactoryTest { void testStrategy_MultipartFile(int sizeMB, StrategyType expected) throws IOException { byte[] inflated = inflatePdf(basePdfBytes, sizeMB); MockMultipartFile multipart = - new MockMultipartFile("file", "doc.pdf", "application/pdf", inflated); + new MockMultipartFile("file", "doc.pdf", MediaType.APPLICATION_PDF_VALUE, inflated); try (PDDocument doc = factory.load(multipart)) { Assertions.assertEquals(expected, factory.lastStrategyUsed); } @@ -84,7 +86,7 @@ class CustomPDFDocumentFactoryTest { void testStrategy_PDFFile(int sizeMB, StrategyType expected) throws IOException { byte[] inflated = inflatePdf(basePdfBytes, sizeMB); MockMultipartFile multipart = - new MockMultipartFile("file", "doc.pdf", "application/pdf", inflated); + new MockMultipartFile("file", "doc.pdf", MediaType.APPLICATION_PDF_VALUE, inflated); PDFFile pdfFile = new PDFFile(); pdfFile.setFileInput(multipart); try (PDDocument doc = factory.load(pdfFile)) { diff --git a/app/common/src/test/java/stirling/software/common/service/FileStorageTest.java b/app/common/src/test/java/stirling/software/common/service/FileStorageTest.java index 76f3e353e..500eca062 100644 --- a/app/common/src/test/java/stirling/software/common/service/FileStorageTest.java +++ b/app/common/src/test/java/stirling/software/common/service/FileStorageTest.java @@ -2,6 +2,8 @@ package stirling.software.common.service; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.AdditionalAnswers.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; import java.io.IOException; @@ -15,6 +17,7 @@ import org.junit.jupiter.api.io.TempDir; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.springframework.http.MediaType; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.web.multipart.MultipartFile; @@ -36,7 +39,7 @@ class FileStorageTest { // Create a mock MultipartFile mockFile = mock(MultipartFile.class); when(mockFile.getOriginalFilename()).thenReturn("test.pdf"); - when(mockFile.getContentType()).thenReturn("application/pdf"); + when(mockFile.getContentType()).thenReturn(MediaType.APPLICATION_PDF_VALUE); } @Test diff --git a/app/common/src/test/java/stirling/software/common/service/TaskManagerTest.java b/app/common/src/test/java/stirling/software/common/service/TaskManagerTest.java index 5b8027b62..15aa46bc8 100644 --- a/app/common/src/test/java/stirling/software/common/service/TaskManagerTest.java +++ b/app/common/src/test/java/stirling/software/common/service/TaskManagerTest.java @@ -13,6 +13,7 @@ import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.springframework.http.MediaType; import org.springframework.test.util.ReflectionTestUtils; import stirling.software.common.model.job.JobResult; @@ -77,7 +78,7 @@ class TaskManagerTest { taskManager.createTask(jobId); String fileId = "file-id"; String originalFileName = "test.pdf"; - String contentType = "application/pdf"; + String contentType = MediaType.APPLICATION_PDF_VALUE; long fileSize = 1024L; // Mock the fileStorage.getFileSize() call @@ -185,7 +186,8 @@ class TaskManagerTest { // 2. Create completed successful job with file String successFileJobId = "success-file-job"; taskManager.createTask(successFileJobId); - taskManager.setFileResult(successFileJobId, "file-id", "test.pdf", "application/pdf"); + taskManager.setFileResult( + successFileJobId, "file-id", "test.pdf", MediaType.APPLICATION_PDF_VALUE); // 3. Create completed successful job without file String successJobId = "success-job"; @@ -235,7 +237,7 @@ class TaskManagerTest { ResultFile.builder() .fileId("file-id") .fileName("test.pdf") - .contentType("application/pdf") + .contentType(MediaType.APPLICATION_PDF_VALUE) .fileSize(1024L) .build(); ReflectionTestUtils.setField(oldJob, "resultFiles", java.util.List.of(resultFile)); diff --git a/app/common/src/test/java/stirling/software/common/util/ChecksumUtilsTest.java b/app/common/src/test/java/stirling/software/common/util/ChecksumUtilsTest.java new file mode 100644 index 000000000..0a5d20a33 --- /dev/null +++ b/app/common/src/test/java/stirling/software/common/util/ChecksumUtilsTest.java @@ -0,0 +1,66 @@ +package stirling.software.common.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +public class ChecksumUtilsTest { + + @Test + void computeChecksums_basic() throws Exception { + byte[] data = "hello".getBytes(StandardCharsets.UTF_8); + + // MD5 (hex) + try (InputStream is = new ByteArrayInputStream(data)) { + assertEquals("5d41402abc4b2a76b9719d911017c592", ChecksumUtils.checksum(is, "MD5")); + } + + // MD5 (Base64) + try (InputStream is = new ByteArrayInputStream(data)) { + assertEquals("XUFAKrxLKna5cZ2REBfFkg==", ChecksumUtils.checksumBase64(is, "MD5")); + } + + // MD5 + CRC32 (hex map) + try (InputStream is = new ByteArrayInputStream(data)) { + Map map = ChecksumUtils.checksums(is, "MD5", "CRC32"); + assertEquals("5d41402abc4b2a76b9719d911017c592", map.get("MD5")); + assertEquals("3610a686", map.get("CRC32")); + } + } + + @Test + void crc32_base64_bigEndianBytes_forHello() throws Exception { + // CRC32("hello") = 0x3610A686 → bytes: 36 10 A6 86 → Base64: "NhCmhg==" + byte[] data = "hello".getBytes(StandardCharsets.UTF_8); + try (InputStream is = new ByteArrayInputStream(data)) { + assertEquals("NhCmhg==", ChecksumUtils.checksumBase64(is, "CRC32")); + } + } + + @Test + void crc32_unsignedFormatting_highBitSet() throws Exception { + // CRC32 of single zero byte (0x00) is 0xD202EF8D (>= 0x8000_0000) + byte[] data = new byte[] {0x00}; + + // Hex (unsigned, 8 chars, lowercase) + try (InputStream is = new ByteArrayInputStream(data)) { + assertEquals("d202ef8d", ChecksumUtils.checksum(is, "CRC32")); + } + + // Base64 of the 4-byte big-endian representation + try (InputStream is = new ByteArrayInputStream(data)) { + assertEquals("0gLvjQ==", ChecksumUtils.checksumBase64(is, "CRC32")); + } + + // matches(..) must be case-insensitive for hex + try (InputStream is = new ByteArrayInputStream("hello".getBytes(StandardCharsets.UTF_8))) { + assertTrue(ChecksumUtils.matches(is, "CRC32", "3610A686")); // uppercase expected + } + } +} diff --git a/app/common/src/test/java/stirling/software/common/util/EmlToPdfTest.java b/app/common/src/test/java/stirling/software/common/util/EmlToPdfTest.java index 952bc692a..9385d260c 100644 --- a/app/common/src/test/java/stirling/software/common/util/EmlToPdfTest.java +++ b/app/common/src/test/java/stirling/software/common/util/EmlToPdfTest.java @@ -586,7 +586,10 @@ class EmlToPdfTest { when(mockPdDocument.getNumberOfPages()).thenReturn(1); try (MockedStatic fileToPdf = - mockStatic(FileToPdf.class, org.mockito.Mockito.withSettings().lenient())) { + mockStatic( + FileToPdf.class, + org.mockito.Mockito.withSettings() + .defaultAnswer(org.mockito.Answers.RETURNS_DEFAULTS))) { fileToPdf .when( () -> @@ -657,7 +660,10 @@ class EmlToPdfTest { when(mockPdDocument.getNumberOfPages()).thenReturn(1); try (MockedStatic fileToPdf = - mockStatic(FileToPdf.class, org.mockito.Mockito.withSettings().lenient())) { + mockStatic( + FileToPdf.class, + org.mockito.Mockito.withSettings() + .defaultAnswer(org.mockito.Answers.RETURNS_DEFAULTS))) { fileToPdf .when( () -> @@ -724,7 +730,10 @@ class EmlToPdfTest { String errorMessage = "Conversion failed"; try (MockedStatic fileToPdf = - mockStatic(FileToPdf.class, org.mockito.Mockito.withSettings().lenient())) { + mockStatic( + FileToPdf.class, + org.mockito.Mockito.withSettings() + .defaultAnswer(org.mockito.Answers.RETURNS_DEFAULTS))) { fileToPdf .when( () -> diff --git a/app/common/src/test/java/stirling/software/common/util/FileInfoTest.java b/app/common/src/test/java/stirling/software/common/util/FileInfoTest.java deleted file mode 100644 index ec991f07e..000000000 --- a/app/common/src/test/java/stirling/software/common/util/FileInfoTest.java +++ /dev/null @@ -1,35 +0,0 @@ -package stirling.software.common.util; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.time.LocalDateTime; - -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; - -import stirling.software.common.model.FileInfo; - -public class FileInfoTest { - - @ParameterizedTest(name = "{index}: fileSize={0}") - @CsvSource({ - "0, '0 Bytes'", - "1023, '1023 Bytes'", - "1024, '1.00 KB'", - "1048575, '1024.00 KB'", // Do we really want this as result? - "1048576, '1.00 MB'", - "1073741823, '1024.00 MB'", // Do we really want this as result? - "1073741824, '1.00 GB'" - }) - void testGetFormattedFileSize(long fileSize, String expectedFormattedSize) { - FileInfo fileInfo = - new FileInfo( - "example.txt", - "/path/to/example.txt", - LocalDateTime.now(), - fileSize, - LocalDateTime.now().minusDays(1)); - - assertEquals(expectedFormattedSize, fileInfo.getFormattedFileSize()); - } -} diff --git a/app/common/src/test/java/stirling/software/common/util/GeneralUtilsTest.java b/app/common/src/test/java/stirling/software/common/util/GeneralUtilsTest.java index a73cd332b..bf6be29e3 100644 --- a/app/common/src/test/java/stirling/software/common/util/GeneralUtilsTest.java +++ b/app/common/src/test/java/stirling/software/common/util/GeneralUtilsTest.java @@ -1,8 +1,15 @@ package stirling.software.common.util; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.*; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.Test; @@ -154,4 +161,223 @@ public class GeneralUtilsTest { List result = GeneralUtils.parsePageList(new String[] {"1,3,7-8"}, 8, false); assertEquals(List.of(0, 2, 6, 7), result, "Range should be parsed correctly."); } + + @Test + void testRemoveExtension() { + // Test common cases (should use fast string operations) + assertEquals("document", GeneralUtils.removeExtension("document.pdf")); + assertEquals("image", GeneralUtils.removeExtension("image.jpg")); + assertEquals("file.backup", GeneralUtils.removeExtension("file.backup.zip")); + assertEquals("complex.file.name", GeneralUtils.removeExtension("complex.file.name.txt")); + + // Test edge cases (should fall back to regex) + assertEquals("default", GeneralUtils.removeExtension(null)); + assertEquals("noextension", GeneralUtils.removeExtension("noextension")); + assertEquals( + ".hidden", GeneralUtils.removeExtension(".hidden")); // Hidden file, no extension + assertEquals("trailing.", GeneralUtils.removeExtension("trailing.")); // Trailing dot + assertEquals("", GeneralUtils.removeExtension("")); + assertEquals("a", GeneralUtils.removeExtension("a")); + + // Test multiple dots + assertEquals("file.with.multiple", GeneralUtils.removeExtension("file.with.multiple.dots")); + assertEquals("path/to/file", GeneralUtils.removeExtension("path/to/file.ext")); + } + + @Test + void testAppendSuffix() { + // Normal cases + assertEquals("document_processed", GeneralUtils.appendSuffix("document", "_processed")); + assertEquals("file.txt", GeneralUtils.appendSuffix("file", ".txt")); + + // Null handling + assertEquals("default_suffix", GeneralUtils.appendSuffix(null, "_suffix")); + assertEquals("basename", GeneralUtils.appendSuffix("basename", null)); + assertEquals("default", GeneralUtils.appendSuffix(null, null)); + + // Empty strings + assertEquals("_suffix", GeneralUtils.appendSuffix("", "_suffix")); + assertEquals("basename", GeneralUtils.appendSuffix("basename", "")); + } + + @Test + void testProcessFilenames() { + List filenames = new ArrayList<>(); + filenames.add("document.pdf"); + filenames.add("image.jpg"); + filenames.add("spreadsheet.xlsx"); + filenames.add("presentation.pptx"); + filenames.add(null); // Should handle null gracefully + filenames.add("noextension"); + + List results = new ArrayList<>(); + GeneralUtils.processFilenames(filenames, "_processed", results::add); + + List expected = + List.of( + "document_processed", + "image_processed", + "spreadsheet_processed", + "presentation_processed", + "default_processed", + "noextension_processed"); + + assertEquals(expected, results); + } + + @Test + void testProcessFilenamesNullHandling() { + List results = new ArrayList<>(); + + // Null filenames list + GeneralUtils.processFilenames(null, "_suffix", results::add); + assertTrue(results.isEmpty(), "Should handle null filenames list"); + + // Null processor + List filenames = List.of("test.txt"); + GeneralUtils.processFilenames(filenames, "_suffix", null); // Should not throw + } + + @Test + void testRemoveExtensionThreadSafety() throws InterruptedException { + final int threadCount = 50; + final int operationsPerThread = 100; + final String[] testFilenames = { + "document.pdf", "image.jpg", "data.csv", "presentation.pptx", + "archive.zip", "music.mp3", "video.mp4", "text.txt" + }; + + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + CountDownLatch latch = new CountDownLatch(threadCount); + AtomicInteger successCount = new AtomicInteger(0); + List exceptions = Collections.synchronizedList(new ArrayList<>()); + + for (int i = 0; i < threadCount; i++) { + executor.submit( + () -> { + try { + for (int j = 0; j < operationsPerThread; j++) { + String filename = testFilenames[j % testFilenames.length]; + String result = GeneralUtils.removeExtension(filename); + + // Verify result is correct + assertFalse( + result.contains("."), + "Result should not contain extension: " + result); + assertTrue( + filename.startsWith(result), + "Original should start with result: " + + filename + + " -> " + + result); + } + successCount.incrementAndGet(); + } catch (Exception e) { + exceptions.add(e); + } finally { + latch.countDown(); + } + }); + } + + assertTrue(latch.await(10, TimeUnit.SECONDS), "All threads should complete"); + + if (!exceptions.isEmpty()) { + fail("Thread safety test failed with exceptions: " + exceptions); + } + + assertEquals(threadCount, successCount.get(), "All threads should succeed"); + + executor.shutdown(); + } + + @Test + void testBatchProcessingPerformance() { + List filenames = new ArrayList<>(); + for (int i = 0; i < 1000; i++) { + filenames.add("file" + i + ".pdf"); + filenames.add("document" + i + ".docx"); + filenames.add("image" + i + ".jpg"); + } + + List results = new ArrayList<>(); + + GeneralUtils.processFilenames(filenames, "_processed", results::add); + + assertEquals(filenames.size(), results.size(), "Should process all filenames"); + + assertTrue(results.contains("file0_processed"), "Should contain processed filename"); + assertTrue(results.contains("document500_processed"), "Should contain processed filename"); + assertTrue(results.contains("image999_processed"), "Should contain processed filename"); + } + + @Test + void testHybridStringRegexApproach() { + + String[] edgeCases = { + "", // Empty string + ".", // Just a dot + "..", // Two dots + "...", // Three dots + ".hidden", // Hidden file + "file.", // Trailing dot + "a.b.c.d.e.f.g", // Many extensions + "no-extension-here", // No extension + "file..double.dot" // Double dots + }; + + for (String edgeCase : edgeCases) { + String result = GeneralUtils.removeExtension(edgeCase); + assertNotNull(result, "Result should not be null for: " + edgeCase); + + // For specific edge cases, verify expected behavior + switch (edgeCase) { + case "" -> assertEquals("", result, "Empty string should remain empty"); + case "." -> assertEquals(".", result, "Single dot should remain unchanged"); + case ".." -> assertEquals("..", result, "Double dots should remain unchanged"); + case "..." -> assertEquals("...", result, "Triple dots should remain unchanged"); + case ".hidden" -> + assertEquals(".hidden", result, "Hidden file should remain unchanged"); + case "file." -> + assertEquals("file.", result, "Trailing dot should remain unchanged"); + case "no-extension-here" -> + assertEquals( + "no-extension-here", + result, + "No extension should remain unchanged"); + case "a.b.c.d.e.f.g" -> + assertEquals( + "a.b.c.d.e.f", + result, + "Multiple extensions should remove last one"); + case "file..double.dot" -> + assertEquals( + "file..double", + result, + "Double dot case should remove last extension"); + } + } + } + + @Test + void testGetTitleFromFilename() { + // Test normal cases + assertEquals("document", GeneralUtils.getTitleFromFilename("document.pdf")); + assertEquals("presentation", GeneralUtils.getTitleFromFilename("presentation.pptx")); + assertEquals("file.backup", GeneralUtils.getTitleFromFilename("file.backup.zip")); + + // Test null and empty handling + assertEquals("Untitled", GeneralUtils.getTitleFromFilename(null)); + assertEquals("Untitled", GeneralUtils.getTitleFromFilename("")); + + // Test edge cases + assertEquals(".hidden", GeneralUtils.getTitleFromFilename(".hidden")); + assertEquals("file.", GeneralUtils.getTitleFromFilename("file.")); + assertEquals("noextension", GeneralUtils.getTitleFromFilename("noextension")); + + // Test complex cases + assertEquals( + "complex.file.name", GeneralUtils.getTitleFromFilename("complex.file.name.txt")); + assertEquals("path/to/file", GeneralUtils.getTitleFromFilename("path/to/file.ext")); + } } diff --git a/app/common/src/test/java/stirling/software/common/util/PDFToFileTest.java b/app/common/src/test/java/stirling/software/common/util/PDFToFileTest.java index 6f4b4af92..2ebb58c0d 100644 --- a/app/common/src/test/java/stirling/software/common/util/PDFToFileTest.java +++ b/app/common/src/test/java/stirling/software/common/util/PDFToFileTest.java @@ -5,7 +5,9 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.when; @@ -25,6 +27,7 @@ import org.mockito.Mock; import org.mockito.MockedStatic; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; import org.springframework.web.multipart.MultipartFile; @@ -46,10 +49,21 @@ class PDFToFileTest { @Mock private ProcessExecutor mockProcessExecutor; @Mock private ProcessExecutorResult mockExecutorResult; + @Mock private TempFileManager mockTempFileManager; @BeforeEach - void setUp() { - pdfToFile = new PDFToFile(); + void setUp() throws IOException { + // Mock the TempFileManager to return real temp files + lenient() + .when(mockTempFileManager.createTempFile(anyString())) + .thenAnswer( + invocation -> + Files.createTempFile("test", invocation.getArgument(0)).toFile()); + lenient() + .when(mockTempFileManager.createTempDirectory()) + .thenAnswer(invocation -> Files.createTempDirectory("test")); + + pdfToFile = new PDFToFile(mockTempFileManager); } @Test @@ -57,7 +71,10 @@ class PDFToFileTest { // Prepare MultipartFile nonPdfFile = new MockMultipartFile( - "file", "test.txt", "text/plain", "This is not a PDF".getBytes()); + "file", + "test.txt", + MediaType.TEXT_PLAIN_VALUE, + "This is not a PDF".getBytes()); // Execute ResponseEntity response = pdfToFile.processPdfToMarkdown(nonPdfFile); @@ -71,7 +88,10 @@ class PDFToFileTest { // Prepare MultipartFile nonPdfFile = new MockMultipartFile( - "file", "test.txt", "text/plain", "This is not a PDF".getBytes()); + "file", + "test.txt", + MediaType.TEXT_PLAIN_VALUE, + "This is not a PDF".getBytes()); // Execute ResponseEntity response = pdfToFile.processPdfToHtml(nonPdfFile); @@ -86,7 +106,10 @@ class PDFToFileTest { // Prepare MultipartFile nonPdfFile = new MockMultipartFile( - "file", "test.txt", "text/plain", "This is not a PDF".getBytes()); + "file", + "test.txt", + MediaType.TEXT_PLAIN_VALUE, + "This is not a PDF".getBytes()); // Execute ResponseEntity response = @@ -102,7 +125,10 @@ class PDFToFileTest { // Prepare MultipartFile pdfFile = new MockMultipartFile( - "file", "test.pdf", "application/pdf", "Fake PDF content".getBytes()); + "file", + "test.pdf", + MediaType.APPLICATION_PDF_VALUE, + "Fake PDF content".getBytes()); // Execute with invalid format ResponseEntity response = @@ -120,7 +146,10 @@ class PDFToFileTest { // Create a mock PDF file MultipartFile pdfFile = new MockMultipartFile( - "file", "test.pdf", "application/pdf", "Fake PDF content".getBytes()); + "file", + "test.pdf", + MediaType.APPLICATION_PDF_VALUE, + "Fake PDF content".getBytes()); // Create a mock HTML output file Path htmlOutputFile = tempDir.resolve("test.html"); @@ -168,7 +197,7 @@ class PDFToFileTest { new MockMultipartFile( "file", "multipage.pdf", - "application/pdf", + MediaType.APPLICATION_PDF_VALUE, "Fake PDF content".getBytes()); // Setup ProcessExecutor mock @@ -245,7 +274,10 @@ class PDFToFileTest { // Create a mock PDF file MultipartFile pdfFile = new MockMultipartFile( - "file", "test.pdf", "application/pdf", "Fake PDF content".getBytes()); + "file", + "test.pdf", + MediaType.APPLICATION_PDF_VALUE, + "Fake PDF content".getBytes()); // Setup ProcessExecutor mock mockedStaticProcessExecutor @@ -324,7 +356,7 @@ class PDFToFileTest { new MockMultipartFile( "file", "document.pdf", - "application/pdf", + MediaType.APPLICATION_PDF_VALUE, "Fake PDF content".getBytes()); // Setup ProcessExecutor mock @@ -386,7 +418,7 @@ class PDFToFileTest { new MockMultipartFile( "file", "document.pdf", - "application/pdf", + MediaType.APPLICATION_PDF_VALUE, "Fake PDF content".getBytes()); // Setup ProcessExecutor mock @@ -472,7 +504,7 @@ class PDFToFileTest { new MockMultipartFile( "file", "document.pdf", - "application/pdf", + MediaType.APPLICATION_PDF_VALUE, "Fake PDF content".getBytes()); // Setup ProcessExecutor mock @@ -531,7 +563,10 @@ class PDFToFileTest { // Create a mock PDF file with no filename MultipartFile pdfFile = new MockMultipartFile( - "file", "", "application/pdf", "Fake PDF content".getBytes()); + "file", + "", + MediaType.APPLICATION_PDF_VALUE, + "Fake PDF content".getBytes()); // Setup ProcessExecutor mock mockedStaticProcessExecutor diff --git a/app/common/src/test/java/stirling/software/common/util/PdfUtilsTest.java b/app/common/src/test/java/stirling/software/common/util/PdfUtilsTest.java index 1b598fc8b..bc68ecb2f 100644 --- a/app/common/src/test/java/stirling/software/common/util/PdfUtilsTest.java +++ b/app/common/src/test/java/stirling/software/common/util/PdfUtilsTest.java @@ -65,23 +65,22 @@ public class PdfUtilsTest { doc1.addPage(new PDPage()); doc1.addPage(new PDPage()); doc1.addPage(new PDPage()); - PdfUtils utils = new PdfUtils(); - assertTrue(utils.pageCount(doc1, 2, "greater")); + assertTrue(PdfUtils.pageCount(doc1, 2, "greater")); PDDocument doc2 = new PDDocument(); doc2.addPage(new PDPage()); doc2.addPage(new PDPage()); doc2.addPage(new PDPage()); - assertTrue(utils.pageCount(doc2, 3, "equal")); + assertTrue(PdfUtils.pageCount(doc2, 3, "equal")); PDDocument doc3 = new PDDocument(); doc3.addPage(new PDPage()); doc3.addPage(new PDPage()); - assertTrue(utils.pageCount(doc3, 5, "less")); + assertTrue(PdfUtils.pageCount(doc3, 5, "less")); PDDocument doc4 = new PDDocument(); doc4.addPage(new PDPage()); - assertThrows(IllegalArgumentException.class, () -> utils.pageCount(doc4, 1, "bad")); + assertThrows(IllegalArgumentException.class, () -> PdfUtils.pageCount(doc4, 1, "bad")); } @Test @@ -91,8 +90,7 @@ public class PdfUtilsTest { doc.addPage(page); PDRectangle rect = page.getMediaBox(); String expected = rect.getWidth() + "x" + rect.getHeight(); - PdfUtils utils = new PdfUtils(); - assertTrue(utils.pageSize(doc, expected)); + assertTrue(PdfUtils.pageSize(doc, expected)); } @Test diff --git a/app/common/src/test/java/stirling/software/common/util/RegexPatternUtilsTest.java b/app/common/src/test/java/stirling/software/common/util/RegexPatternUtilsTest.java new file mode 100644 index 000000000..a691cd42b --- /dev/null +++ b/app/common/src/test/java/stirling/software/common/util/RegexPatternUtilsTest.java @@ -0,0 +1,115 @@ +package stirling.software.common.util; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.regex.Pattern; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class RegexPatternUtilsTest { + + private RegexPatternUtils utils; + + @BeforeEach + void setUp() { + utils = RegexPatternUtils.getInstance(); + utils.clearCache(); // Start with clean cache for each test + } + + @Test + void testPatternCaching() { + String regex = "test\\d+"; + + Pattern pattern1 = utils.getPattern(regex); + assertNotNull(pattern1); + assertTrue(utils.isCached(regex)); + assertEquals( + 1, utils.getCacheSize()); // Should have at least 1 pattern (plus precompiled ones + // are cleared) + + Pattern pattern2 = utils.getPattern(regex); + assertSame(pattern1, pattern2); // Should be the exact same object + } + + @Test + void testPatternWithFlags() { + String regex = "test"; + int flags = Pattern.CASE_INSENSITIVE; + + Pattern pattern1 = utils.getPattern(regex, flags); + Pattern pattern2 = utils.getPattern(regex); // No flags + + assertNotSame(pattern1, pattern2); // Different flags = different cached patterns + assertTrue(utils.isCached(regex, flags)); + assertTrue(utils.isCached(regex, 0)); + } + + @Test + void testCacheEviction() { + String regex = "evict\\d+"; + + utils.getPattern(regex); + assertTrue(utils.isCached(regex)); + + boolean removed = utils.removeFromCache(regex); + assertTrue(removed); + assertFalse(utils.isCached(regex)); + + boolean removedAgain = utils.removeFromCache(regex); + assertFalse(removedAgain); + } + + @Test + void testNullRegexHandling() { + assertThrows( + IllegalArgumentException.class, + () -> { + utils.getPattern(null); + }); + + assertThrows( + IllegalArgumentException.class, + () -> { + utils.getPattern(null, Pattern.CASE_INSENSITIVE); + }); + + assertFalse(utils.isCached(null)); + assertFalse(utils.removeFromCache(null)); + } + + @Test + void testCommonPatterns() { + Pattern whitespace = utils.getWhitespacePattern(); + assertTrue(whitespace.matcher(" \t ").matches()); + + Pattern trailing = utils.getTrailingSlashesPattern(); + assertTrue(trailing.matcher("/path/to/dir///").find()); + + Pattern filename = utils.getSafeFilenamePattern(); + assertTrue(filename.matcher("badname").find()); + } + + @Test + void testCreateSearchPattern() { + String regex = "Hello"; + + Pattern caseSensitive = utils.createSearchPattern(regex, false); + Pattern caseInsensitive = utils.createSearchPattern(regex, true); + + assertTrue(caseSensitive.matcher("Hello").matches()); + assertFalse(caseSensitive.matcher("hello").matches()); + + assertTrue(caseInsensitive.matcher("Hello").matches()); + assertTrue(caseInsensitive.matcher("hello").matches()); + assertTrue(caseInsensitive.matcher("HELLO").matches()); + } + + @Test + void testSingletonBehavior() { + RegexPatternUtils instance1 = RegexPatternUtils.getInstance(); + RegexPatternUtils instance2 = RegexPatternUtils.getInstance(); + + assertSame(instance1, instance2); + } +} diff --git a/app/common/src/test/java/stirling/software/common/util/WebResponseUtilsTest.java b/app/common/src/test/java/stirling/software/common/util/WebResponseUtilsTest.java index 70286fbf7..6b716442e 100644 --- a/app/common/src/test/java/stirling/software/common/util/WebResponseUtilsTest.java +++ b/app/common/src/test/java/stirling/software/common/util/WebResponseUtilsTest.java @@ -48,7 +48,8 @@ public class WebResponseUtilsTest { try { byte[] fileContent = "Sample file content".getBytes(); MockMultipartFile file = - new MockMultipartFile("file", "sample.txt", "text/plain", fileContent); + new MockMultipartFile( + "file", "sample.txt", MediaType.TEXT_PLAIN_VALUE, fileContent); ResponseEntity responseEntity = WebResponseUtils.multiPartFileToWebResponse(file); diff --git a/app/common/src/test/java/stirling/software/common/util/misc/CustomColorReplaceStrategyTest.java b/app/common/src/test/java/stirling/software/common/util/misc/CustomColorReplaceStrategyTest.java index 4ea57e92f..c50cc335c 100644 --- a/app/common/src/test/java/stirling/software/common/util/misc/CustomColorReplaceStrategyTest.java +++ b/app/common/src/test/java/stirling/software/common/util/misc/CustomColorReplaceStrategyTest.java @@ -8,6 +8,7 @@ import java.lang.reflect.Method; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; import org.springframework.mock.web.MockMultipartFile; import org.springframework.web.multipart.MultipartFile; @@ -24,7 +25,10 @@ class CustomColorReplaceStrategyTest { // Create a mock file mockFile = new MockMultipartFile( - "file", "test.pdf", "application/pdf", "test pdf content".getBytes()); + "file", + "test.pdf", + MediaType.APPLICATION_PDF_VALUE, + "test pdf content".getBytes()); // Initialize strategy with custom colors strategy = diff --git a/app/common/src/test/java/stirling/software/common/util/misc/InvertFullColorStrategyTest.java b/app/common/src/test/java/stirling/software/common/util/misc/InvertFullColorStrategyTest.java index 61b48e4f7..7bdf4f612 100644 --- a/app/common/src/test/java/stirling/software/common/util/misc/InvertFullColorStrategyTest.java +++ b/app/common/src/test/java/stirling/software/common/util/misc/InvertFullColorStrategyTest.java @@ -24,6 +24,7 @@ import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceRGB; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.core.io.InputStreamResource; +import org.springframework.http.MediaType; import org.springframework.mock.web.MockMultipartFile; import org.springframework.web.multipart.MultipartFile; @@ -38,7 +39,9 @@ class InvertFullColorStrategyTest { void setUp() throws Exception { // Create a simple PDF document for testing byte[] pdfBytes = createSimplePdfWithRectangle(); - mockPdfFile = new MockMultipartFile("file", "test.pdf", "application/pdf", pdfBytes); + mockPdfFile = + new MockMultipartFile( + "file", "test.pdf", MediaType.APPLICATION_PDF_VALUE, pdfBytes); // Create the strategy instance strategy = new InvertFullColorStrategy(mockPdfFile, ReplaceAndInvert.FULL_INVERSION); diff --git a/app/common/src/test/java/stirling/software/common/util/misc/ReplaceAndInvertColorStrategyTest.java b/app/common/src/test/java/stirling/software/common/util/misc/ReplaceAndInvertColorStrategyTest.java index 0f9471773..e5ce10fb2 100644 --- a/app/common/src/test/java/stirling/software/common/util/misc/ReplaceAndInvertColorStrategyTest.java +++ b/app/common/src/test/java/stirling/software/common/util/misc/ReplaceAndInvertColorStrategyTest.java @@ -7,6 +7,7 @@ import java.io.IOException; import org.junit.jupiter.api.Test; import org.springframework.core.io.InputStreamResource; +import org.springframework.http.MediaType; import org.springframework.mock.web.MockMultipartFile; import org.springframework.web.multipart.MultipartFile; @@ -35,7 +36,10 @@ class ReplaceAndInvertColorStrategyTest { // Arrange MultipartFile mockFile = new MockMultipartFile( - "file", "test.pdf", "application/pdf", "test content".getBytes()); + "file", + "test.pdf", + MediaType.APPLICATION_PDF_VALUE, + "test content".getBytes()); ReplaceAndInvert replaceAndInvert = ReplaceAndInvert.CUSTOM_COLOR; // Act @@ -56,7 +60,7 @@ class ReplaceAndInvertColorStrategyTest { // Arrange byte[] content = "test pdf content".getBytes(); MultipartFile mockFile = - new MockMultipartFile("file", "test.pdf", "application/pdf", content); + new MockMultipartFile("file", "test.pdf", MediaType.APPLICATION_PDF_VALUE, content); ReplaceAndInvert replaceAndInvert = ReplaceAndInvert.CUSTOM_COLOR; ReplaceAndInvertColorStrategy strategy = @@ -74,10 +78,16 @@ class ReplaceAndInvertColorStrategyTest { // Arrange MultipartFile mockFile1 = new MockMultipartFile( - "file1", "test1.pdf", "application/pdf", "content1".getBytes()); + "file1", + "test1.pdf", + MediaType.APPLICATION_PDF_VALUE, + "content1".getBytes()); MultipartFile mockFile2 = new MockMultipartFile( - "file2", "test2.pdf", "application/pdf", "content2".getBytes()); + "file2", + "test2.pdf", + MediaType.APPLICATION_PDF_VALUE, + "content2".getBytes()); // Act ReplaceAndInvertColorStrategy strategy = diff --git a/app/core/.gitignore b/app/core/.gitignore index 5486f9afe..7d9dd6293 100644 --- a/app/core/.gitignore +++ b/app/core/.gitignore @@ -170,6 +170,10 @@ out/ *.jks *.asc +# test-cert +!**/test/resources/certs/test-cert.* +!**/test/resources/certs/test-key.* + # SSH Keys *.pub *.priv diff --git a/app/core/build.gradle b/app/core/build.gradle index c9905a308..59b007a3a 100644 --- a/app/core/build.gradle +++ b/app/core/build.gradle @@ -58,7 +58,7 @@ dependencies { implementation 'commons-io:commons-io:2.20.0' implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion" implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion" - implementation 'io.micrometer:micrometer-core:1.15.2' + implementation 'io.micrometer:micrometer-core:1.15.4' implementation 'com.google.zxing:core:3.5.3' implementation "org.commonmark:commonmark:$commonmarkVersion" // https://mvnrepository.com/artifact/org.commonmark/commonmark implementation "org.commonmark:commonmark-ext-gfm-tables:$commonmarkVersion" @@ -114,11 +114,6 @@ sourceSets { } test { java { - if (System.getenv('DOCKER_ENABLE_SECURITY') == 'false' || System.getenv('DISABLE_ADDITIONAL_FEATURES') == 'true' - || (project.hasProperty('DISABLE_ADDITIONAL_FEATURES') - && System.getProperty('DISABLE_ADDITIONAL_FEATURES') == 'true')) { - exclude 'stirling/software/proprietary/security/**' - } if (System.getenv('STIRLING_PDF_DESKTOP_UI') == 'false') { exclude 'stirling/software/SPDF/UI/impl/**' } diff --git a/app/core/src/main/java/org/apache/pdfbox/examples/signature/CreateSignatureBase.java b/app/core/src/main/java/org/apache/pdfbox/examples/signature/CreateSignatureBase.java index aba11d9b0..289f31501 100644 --- a/app/core/src/main/java/org/apache/pdfbox/examples/signature/CreateSignatureBase.java +++ b/app/core/src/main/java/org/apache/pdfbox/examples/signature/CreateSignatureBase.java @@ -42,11 +42,25 @@ import org.bouncycastle.operator.OperatorCreationException; import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; +import lombok.Getter; +import lombok.Setter; + public abstract class CreateSignatureBase implements SignatureInterface { private PrivateKey privateKey; - private Certificate[] certificateChain; - private String tsaUrl; - private boolean externalSigning; + @Getter private Certificate[] certificateChain; + @Setter private String tsaUrl; + + /** + * Specifies whether the external signing scenario should be used. If set to {@code true}, + * external signing will be performed and {@link SignatureInterface} will be used for signing. + * If set to {@code false}, internal signing will be performed. + * + *

Default: {@code false} + * + * @param externalSigning {@code true} if external signing should be performed; {@code false} + * for internal signing + */ + @Setter @Getter private boolean externalSigning; /** * Initialize the signature creator with a keystore (pkcs12) and pin that should be used for the @@ -97,18 +111,10 @@ public abstract class CreateSignatureBase implements SignatureInterface { this.privateKey = privateKey; } - public Certificate[] getCertificateChain() { - return certificateChain; - } - public final void setCertificateChain(final Certificate[] certificateChain) { this.certificateChain = certificateChain; } - public void setTsaUrl(String tsaUrl) { - this.tsaUrl = tsaUrl; - } - /** * SignatureInterface sample implementation. * @@ -151,20 +157,4 @@ public abstract class CreateSignatureBase implements SignatureInterface { throw new IOException(e); } } - - public boolean isExternalSigning() { - return externalSigning; - } - - /** - * Set if external signing scenario should be used. If {@code false}, SignatureInterface would - * be used for signing. - * - *

Default: {@code false} - * - * @param externalSigning {@code true} if external signing should be performed - */ - public void setExternalSigning(boolean externalSigning) { - this.externalSigning = externalSigning; - } } diff --git a/app/core/src/main/java/stirling/software/SPDF/Factories/ReplaceAndInvertColorFactory.java b/app/core/src/main/java/stirling/software/SPDF/Factories/ReplaceAndInvertColorFactory.java index 49be7fd42..6697beb79 100644 --- a/app/core/src/main/java/stirling/software/SPDF/Factories/ReplaceAndInvertColorFactory.java +++ b/app/core/src/main/java/stirling/software/SPDF/Factories/ReplaceAndInvertColorFactory.java @@ -3,15 +3,22 @@ package stirling.software.SPDF.Factories; import org.springframework.stereotype.Component; import org.springframework.web.multipart.MultipartFile; +import lombok.RequiredArgsConstructor; + import stirling.software.common.model.api.misc.HighContrastColorCombination; import stirling.software.common.model.api.misc.ReplaceAndInvert; +import stirling.software.common.util.TempFileManager; +import stirling.software.common.util.misc.ColorSpaceConversionStrategy; import stirling.software.common.util.misc.CustomColorReplaceStrategy; import stirling.software.common.util.misc.InvertFullColorStrategy; import stirling.software.common.util.misc.ReplaceAndInvertColorStrategy; @Component +@RequiredArgsConstructor public class ReplaceAndInvertColorFactory { + private final TempFileManager tempFileManager; + public ReplaceAndInvertColorStrategy replaceAndInvert( MultipartFile file, ReplaceAndInvert replaceAndInvertOption, @@ -19,21 +26,17 @@ public class ReplaceAndInvertColorFactory { String backGroundColor, String textColor) { - if (replaceAndInvertOption == ReplaceAndInvert.CUSTOM_COLOR - || replaceAndInvertOption == ReplaceAndInvert.HIGH_CONTRAST_COLOR) { - - return new CustomColorReplaceStrategy( - file, - replaceAndInvertOption, - textColor, - backGroundColor, - highContrastColorCombination); - - } else if (replaceAndInvertOption == ReplaceAndInvert.FULL_INVERSION) { - - return new InvertFullColorStrategy(file, replaceAndInvertOption); - } - - return null; + return switch (replaceAndInvertOption) { + case CUSTOM_COLOR, HIGH_CONTRAST_COLOR -> + new CustomColorReplaceStrategy( + file, + replaceAndInvertOption, + textColor, + backGroundColor, + highContrastColorCombination); + case FULL_INVERSION -> new InvertFullColorStrategy(file, replaceAndInvertOption); + case COLOR_SPACE_CONVERSION -> + new ColorSpaceConversionStrategy(file, replaceAndInvertOption, tempFileManager); + }; } } diff --git a/app/core/src/main/java/stirling/software/SPDF/SPDFApplication.java b/app/core/src/main/java/stirling/software/SPDF/SPDFApplication.java index 288d11a8d..4a05b4f15 100644 --- a/app/core/src/main/java/stirling/software/SPDF/SPDFApplication.java +++ b/app/core/src/main/java/stirling/software/SPDF/SPDFApplication.java @@ -46,14 +46,11 @@ public class SPDFApplication { private final Environment env; private final ApplicationProperties applicationProperties; - // private final WebBrowser webBrowser; // Removed - desktop UI eliminated - public SPDFApplication( AppConfig appConfig, Environment env, ApplicationProperties applicationProperties) { this.appConfig = appConfig; this.env = env; this.applicationProperties = applicationProperties; - // this.webBrowser = webBrowser; // Removed - desktop UI eliminated } public static void main(String[] args) throws IOException, InterruptedException { diff --git a/app/core/src/main/java/stirling/software/SPDF/config/AppUpdateService.java b/app/core/src/main/java/stirling/software/SPDF/config/AppUpdateService.java index c4c528f77..544867443 100644 --- a/app/core/src/main/java/stirling/software/SPDF/config/AppUpdateService.java +++ b/app/core/src/main/java/stirling/software/SPDF/config/AppUpdateService.java @@ -26,7 +26,7 @@ class AppUpdateService { @Scope("request") public boolean shouldShow() { boolean showUpdate = applicationProperties.getSystem().isShowUpdate(); - boolean showAdminResult = (showAdmin != null) ? showAdmin.getShowUpdateOnlyAdmins() : true; + boolean showAdminResult = showAdmin == null || showAdmin.getShowUpdateOnlyAdmins(); return showUpdate && showAdminResult; } } diff --git a/app/core/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java b/app/core/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java index d37d4bfb6..785743731 100644 --- a/app/core/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java +++ b/app/core/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java @@ -20,6 +20,8 @@ public class CleanUrlInterceptor implements HandlerInterceptor { "endpoints", "logout", "error", + "days", + "date", "errorOAuth", "file", "messageType", @@ -64,7 +66,7 @@ public class CleanUrlInterceptor implements HandlerInterceptor { // Construct new query string StringBuilder newQueryString = new StringBuilder(); for (Map.Entry entry : allowedParameters.entrySet()) { - if (newQueryString.length() > 0) { + if (!newQueryString.isEmpty()) { newQueryString.append("&"); } newQueryString.append(entry.getKey()).append("=").append(entry.getValue()); diff --git a/app/core/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java b/app/core/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java index a99bc4184..edb2e96cf 100644 --- a/app/core/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java +++ b/app/core/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java @@ -9,6 +9,7 @@ import java.util.concurrent.ConcurrentHashMap; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; import stirling.software.common.model.ApplicationProperties; @@ -19,7 +20,7 @@ public class EndpointConfiguration { private static final String REMOVE_BLANKS = "remove-blanks"; private final ApplicationProperties applicationProperties; - private Map endpointStatuses = new ConcurrentHashMap<>(); + @Getter private Map endpointStatuses = new ConcurrentHashMap<>(); private Map> endpointGroups = new ConcurrentHashMap<>(); private Set disabledGroups = new HashSet<>(); private Map> endpointAlternatives = new ConcurrentHashMap<>(); @@ -46,10 +47,6 @@ public class EndpointConfiguration { endpointStatuses.put(endpoint, false); } - public Map getEndpointStatuses() { - return endpointStatuses; - } - public boolean isEndpointEnabled(String endpoint) { String original = endpoint; if (endpoint.startsWith("/")) { @@ -391,6 +388,7 @@ public class EndpointConfiguration { addEndpointToGroup("Java", "pdf-to-markdown"); addEndpointToGroup("Java", "add-attachments"); addEndpointToGroup("Java", "compress-pdf"); + addEndpointToGroup("rar", "pdf-to-cbr"); // Javascript addEndpointToGroup("Javascript", "pdf-organizer"); @@ -405,6 +403,8 @@ public class EndpointConfiguration { /* Ghostscript */ addEndpointToGroup("Ghostscript", "repair"); addEndpointToGroup("Ghostscript", "compress-pdf"); + addEndpointToGroup("Ghostscript", "crop"); + addEndpointToGroup("Ghostscript", "replace-invert-pdf"); /* tesseract */ addEndpointToGroup("tesseract", "ocr-pdf"); @@ -488,7 +488,8 @@ public class EndpointConfiguration { || "Java".equals(group) || "Javascript".equals(group) || "Weasyprint".equals(group) - || "Pdftohtml".equals(group); + || "Pdftohtml".equals(group) + || "rar".equals(group); } private boolean isEndpointEnabledDirectly(String endpoint) { diff --git a/app/core/src/main/java/stirling/software/SPDF/config/EndpointInspector.java b/app/core/src/main/java/stirling/software/SPDF/config/EndpointInspector.java index d9ceb0f9d..457213412 100644 --- a/app/core/src/main/java/stirling/software/SPDF/config/EndpointInspector.java +++ b/app/core/src/main/java/stirling/software/SPDF/config/EndpointInspector.java @@ -6,8 +6,6 @@ import java.util.Map; import java.util.Set; import java.util.TreeSet; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationListener; import org.springframework.context.event.ContextRefreshedEvent; @@ -18,11 +16,12 @@ import org.springframework.web.servlet.mvc.method.RequestMappingInfo; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; @Component @RequiredArgsConstructor +@Slf4j public class EndpointInspector implements ApplicationListener { - private static final Logger logger = LoggerFactory.getLogger(EndpointInspector.class); private final ApplicationContext applicationContext; private final Set validGetEndpoints = new HashSet<>(); @@ -71,13 +70,13 @@ public class EndpointInspector implements ApplicationListener sortedEndpoints = new TreeSet<>(validGetEndpoints); - logger.info("=== BEGIN: All discovered GET endpoints ==="); + log.info("=== BEGIN: All discovered GET endpoints ==="); for (String endpoint : sortedEndpoints) { - logger.info("Endpoint: {}", endpoint); + log.info("Endpoint: {}", endpoint); } - logger.info("=== END: All discovered GET endpoints ==="); + log.info("=== END: All discovered GET endpoints ==="); } } diff --git a/app/core/src/main/java/stirling/software/SPDF/config/ExternalAppDepConfig.java b/app/core/src/main/java/stirling/software/SPDF/config/ExternalAppDepConfig.java index 6d857c679..fd3ab640d 100644 --- a/app/core/src/main/java/stirling/software/SPDF/config/ExternalAppDepConfig.java +++ b/app/core/src/main/java/stirling/software/SPDF/config/ExternalAppDepConfig.java @@ -13,6 +13,7 @@ import jakarta.annotation.PostConstruct; import lombok.extern.slf4j.Slf4j; import stirling.software.common.configuration.RuntimePathConfig; +import stirling.software.common.util.RegexPatternUtils; @Configuration @Slf4j @@ -42,6 +43,7 @@ public class ExternalAppDepConfig { put(unoconvPath, List.of("Unoconvert")); put("qpdf", List.of("qpdf")); put("tesseract", List.of("tesseract")); + put("rar", List.of("rar")); // Required for real CBR output } }; } @@ -73,7 +75,7 @@ public class ExternalAppDepConfig { // First replace common terms String feature = endpoint.replace("-", " ").replace("pdf", "PDF").replace("img", "image"); // Split into words and capitalize each word - return Arrays.stream(feature.split("\\s+")) + return Arrays.stream(RegexPatternUtils.getInstance().getWordSplitPattern().split(feature)) .map(word -> capitalizeWord(word)) .collect(Collectors.joining(" ")); } @@ -100,7 +102,7 @@ public class ExternalAppDepConfig { "Missing dependency: {} - Disabling group: {} (Affected features: {})", command, group, - affectedFeatures != null && !affectedFeatures.isEmpty() + !affectedFeatures.isEmpty() ? String.join(", ", affectedFeatures) : "unknown"); } @@ -119,6 +121,7 @@ public class ExternalAppDepConfig { checkDependencyAndDisableGroup(weasyprintPath); checkDependencyAndDisableGroup("pdftohtml"); checkDependencyAndDisableGroup(unoconvPath); + checkDependencyAndDisableGroup("rar"); // Special handling for Python/OpenCV dependencies boolean pythonAvailable = isCommandAvailable("python3") || isCommandAvailable("python"); if (!pythonAvailable) { diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/AnalysisController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/AnalysisController.java index 7594e2ab4..13f830b30 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/AnalysisController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/AnalysisController.java @@ -11,6 +11,7 @@ import org.apache.pdfbox.pdmodel.PDPageTree; import org.apache.pdfbox.pdmodel.encryption.PDEncryption; import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation; import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm; +import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.*; import io.swagger.v3.oas.annotations.Operation; @@ -29,7 +30,7 @@ public class AnalysisController { private final CustomPDFDocumentFactory pdfDocumentFactory; - @AutoJobPostMapping(value = "/page-count", consumes = "multipart/form-data") + @AutoJobPostMapping(value = "/page-count", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @JsonDataResponse @Operation( summary = "Get PDF page count", @@ -40,7 +41,7 @@ public class AnalysisController { } } - @AutoJobPostMapping(value = "/basic-info", consumes = "multipart/form-data") + @AutoJobPostMapping(value = "/basic-info", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @JsonDataResponse @Operation( summary = "Get basic PDF information", @@ -55,7 +56,9 @@ public class AnalysisController { } } - @AutoJobPostMapping(value = "/document-properties", consumes = "multipart/form-data") + @AutoJobPostMapping( + value = "/document-properties", + consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @JsonDataResponse @Operation( summary = "Get PDF document properties", @@ -79,7 +82,7 @@ public class AnalysisController { } } - @AutoJobPostMapping(value = "/page-dimensions", consumes = "multipart/form-data") + @AutoJobPostMapping(value = "/page-dimensions", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @JsonDataResponse @Operation( summary = "Get page dimensions for all pages", @@ -100,7 +103,7 @@ public class AnalysisController { } } - @AutoJobPostMapping(value = "/form-fields", consumes = "multipart/form-data") + @AutoJobPostMapping(value = "/form-fields", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @JsonDataResponse @Operation( summary = "Get form field information", @@ -124,7 +127,7 @@ public class AnalysisController { } } - @AutoJobPostMapping(value = "/annotation-info", consumes = "multipart/form-data") + @AutoJobPostMapping(value = "/annotation-info", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @JsonDataResponse @Operation( summary = "Get annotation information", @@ -149,7 +152,7 @@ public class AnalysisController { } } - @AutoJobPostMapping(value = "/font-info", consumes = "multipart/form-data") + @AutoJobPostMapping(value = "/font-info", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @JsonDataResponse @Operation( summary = "Get font information", @@ -172,7 +175,7 @@ public class AnalysisController { } } - @AutoJobPostMapping(value = "/security-info", consumes = "multipart/form-data") + @AutoJobPostMapping(value = "/security-info", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @JsonDataResponse @Operation( summary = "Get security information", diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/BookletImpositionController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/BookletImpositionController.java index d2e278429..12bcdf411 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/BookletImpositionController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/BookletImpositionController.java @@ -13,6 +13,7 @@ import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; import org.apache.pdfbox.util.Matrix; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestMapping; @@ -38,7 +39,9 @@ public class BookletImpositionController { private final CustomPDFDocumentFactory pdfDocumentFactory; - @AutoJobPostMapping(value = "/booklet-imposition", consumes = "multipart/form-data") + @AutoJobPostMapping( + value = "/booklet-imposition", + consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @Operation( summary = "Create a booklet with proper page imposition", description = diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/CropController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/CropController.java index df878fc64..17c3b28ea 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/CropController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/CropController.java @@ -2,6 +2,9 @@ package stirling.software.SPDF.controller.api; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; import org.apache.pdfbox.multipdf.LayerUtility; import org.apache.pdfbox.pdmodel.PDDocument; @@ -10,27 +13,32 @@ import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.PDPageContentStream.AppendMode; import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.config.swagger.StandardPdfResponse; import stirling.software.SPDF.model.api.general.CropPdfForm; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.GeneralApi; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.ProcessExecutor; import stirling.software.common.util.WebResponseUtils; @GeneralApi @RequiredArgsConstructor +@Slf4j public class CropController { private final CustomPDFDocumentFactory pdfDocumentFactory; - @AutoJobPostMapping(value = "/crop", consumes = "multipart/form-data") + @AutoJobPostMapping(value = "/crop", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @StandardPdfResponse @Operation( summary = "Crops a PDF document", @@ -38,6 +46,15 @@ public class CropController { "This operation takes an input PDF file and crops it according to the given" + " coordinates. Input:PDF Output:PDF Type:SISO") public ResponseEntity cropPdf(@ModelAttribute CropPdfForm request) throws IOException { + if (request.isRemoveDataOutsideCrop()) { + return cropWithGhostscript(request); + } else { + return cropWithPDFBox(request); + } + } + + private ResponseEntity cropWithPDFBox(@ModelAttribute CropPdfForm request) + throws IOException { PDDocument sourceDocument = pdfDocumentFactory.load(request); PDDocument newDocument = @@ -90,7 +107,62 @@ public class CropController { byte[] pdfContent = baos.toByteArray(); return WebResponseUtils.bytesToWebResponse( pdfContent, - request.getFileInput().getOriginalFilename().replaceFirst("[.][^.]+$", "") - + "_cropped.pdf"); + GeneralUtils.generateFilename( + request.getFileInput().getOriginalFilename(), "_cropped.pdf")); + } + + private ResponseEntity cropWithGhostscript(@ModelAttribute CropPdfForm request) + throws IOException { + PDDocument sourceDocument = pdfDocumentFactory.load(request); + + for (int i = 0; i < sourceDocument.getNumberOfPages(); i++) { + PDPage page = sourceDocument.getPage(i); + PDRectangle cropBox = + new PDRectangle( + request.getX(), + request.getY(), + request.getWidth(), + request.getHeight()); + page.setCropBox(cropBox); + } + + Path tempInputFile = Files.createTempFile("crop_input", ".pdf"); + Path tempOutputFile = Files.createTempFile("crop_output", ".pdf"); + + try { + sourceDocument.save(tempInputFile.toFile()); + sourceDocument.close(); + + ProcessExecutor processExecutor = + ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT); + List command = + List.of( + "gs", + "-sDEVICE=pdfwrite", + "-dUseCropBox", + "-o", + tempOutputFile.toString(), + tempInputFile.toString()); + + processExecutor.runCommandWithOutputHandling(command); + + byte[] pdfContent = Files.readAllBytes(tempOutputFile); + + return WebResponseUtils.bytesToWebResponse( + pdfContent, + request.getFileInput().getOriginalFilename().replaceFirst("[.][^.]+$", "") + + "_cropped.pdf"); + + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException("Ghostscript processing was interrupted", e); + } finally { + try { + Files.deleteIfExists(tempInputFile); + Files.deleteIfExists(tempOutputFile); + } catch (IOException e) { + log.debug("Failed to delete temporary files", e); + } + } } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/EditTableOfContentsController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/EditTableOfContentsController.java index ae980c47a..481ca16d6 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/EditTableOfContentsController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/EditTableOfContentsController.java @@ -13,6 +13,7 @@ import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlin import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineNode; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; @@ -23,7 +24,9 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.swagger.v3.oas.annotations.Operation; +import lombok.Getter; import lombok.RequiredArgsConstructor; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.config.swagger.JsonDataResponse; @@ -32,6 +35,7 @@ import stirling.software.SPDF.model.api.EditTableOfContentsRequest; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.GeneralApi; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.WebResponseUtils; @GeneralApi @@ -42,7 +46,9 @@ public class EditTableOfContentsController { private final CustomPDFDocumentFactory pdfDocumentFactory; private final ObjectMapper objectMapper; - @AutoJobPostMapping(value = "/extract-bookmarks", consumes = "multipart/form-data") + @AutoJobPostMapping( + value = "/extract-bookmarks", + consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @JsonDataResponse @Operation( summary = "Extract PDF Bookmarks", @@ -151,7 +157,9 @@ public class EditTableOfContentsController { return bookmark; } - @AutoJobPostMapping(value = "/edit-table-of-contents", consumes = "multipart/form-data") + @AutoJobPostMapping( + value = "/edit-table-of-contents", + consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @StandardPdfResponse @Operation( summary = "Edit Table of Contents", @@ -180,9 +188,10 @@ public class EditTableOfContentsController { ByteArrayOutputStream baos = new ByteArrayOutputStream(); document.save(baos); - String filename = file.getOriginalFilename().replaceFirst("[.][^.]+$", ""); return WebResponseUtils.bytesToWebResponse( - baos.toByteArray(), filename + "_with_toc.pdf", MediaType.APPLICATION_PDF); + baos.toByteArray(), + GeneralUtils.generateFilename(file.getOriginalFilename(), "_with_toc.pdf"), + MediaType.APPLICATION_PDF); } finally { if (document != null) { @@ -234,33 +243,11 @@ public class EditTableOfContentsController { } // Inner class to represent bookmarks in JSON + @Setter + @Getter public static class BookmarkItem { private String title; private int pageNumber; private List children = new ArrayList<>(); - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public int getPageNumber() { - return pageNumber; - } - - public void setPageNumber(int pageNumber) { - this.pageNumber = pageNumber; - } - - public List getChildren() { - return children; - } - - public void setChildren(List children) { - this.children = children; - } } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/MergeController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/MergeController.java index 50fc6e0c2..89f1e6912 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/MergeController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/MergeController.java @@ -1,12 +1,8 @@ package stirling.software.SPDF.controller.api; -import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.nio.file.attribute.BasicFileAttributes; +import java.io.InputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; @@ -15,17 +11,23 @@ import java.util.List; import org.apache.pdfbox.multipdf.PDFMergerUtility; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocumentCatalog; +import org.apache.pdfbox.pdmodel.PDDocumentInformation; import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.common.PDMetadata; import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDDocumentOutline; import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineItem; import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm; import org.apache.pdfbox.pdmodel.interactive.form.PDField; import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField; -import org.springframework.http.HttpStatus; +import org.apache.xmpbox.XMPMetadata; +import org.apache.xmpbox.schema.XMPBasicSchema; +import org.apache.xmpbox.xml.DomXmpParser; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -40,6 +42,8 @@ import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.PdfErrorUtils; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @GeneralApi @@ -48,6 +52,7 @@ import stirling.software.common.util.WebResponseUtils; public class MergeController { private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; // Merges a list of PDDocument objects into a single PDDocument public PDDocument mergeDocuments(List documents) throws IOException { @@ -60,58 +65,78 @@ public class MergeController { return mergedDoc; } + // Re-order files to match the explicit order provided by the front-end. + // fileOrder is newline-delimited original filenames in the desired order. + private static MultipartFile[] reorderFilesByProvidedOrder( + MultipartFile[] files, String fileOrder) { + String[] desired = fileOrder.split("\n", -1); + List remaining = new ArrayList<>(Arrays.asList(files)); + List ordered = new ArrayList<>(files.length); + + for (String name : desired) { + if (name == null || name.isEmpty()) continue; + int idx = indexOfByOriginalFilename(remaining, name); + if (idx >= 0) { + ordered.add(remaining.remove(idx)); + } + } + + // Append any files not explicitly listed, preserving their relative order + ordered.addAll(remaining); + return ordered.toArray(new MultipartFile[0]); + } + // Returns a comparator for sorting MultipartFile arrays based on the given sort type private Comparator getSortComparator(String sortType) { - switch (sortType) { - case "byFileName": - return Comparator.comparing(MultipartFile::getOriginalFilename); - case "byDateModified": - return (file1, file2) -> { - try { - BasicFileAttributes attr1 = - Files.readAttributes( - Paths.get(file1.getOriginalFilename()), - BasicFileAttributes.class); - BasicFileAttributes attr2 = - Files.readAttributes( - Paths.get(file2.getOriginalFilename()), - BasicFileAttributes.class); - return attr1.lastModifiedTime().compareTo(attr2.lastModifiedTime()); - } catch (IOException e) { - return 0; // If there's an error, treat them as equal - } - }; - case "byDateCreated": - return (file1, file2) -> { - try { - BasicFileAttributes attr1 = - Files.readAttributes( - Paths.get(file1.getOriginalFilename()), - BasicFileAttributes.class); - BasicFileAttributes attr2 = - Files.readAttributes( - Paths.get(file2.getOriginalFilename()), - BasicFileAttributes.class); - return attr1.creationTime().compareTo(attr2.creationTime()); - } catch (IOException e) { - return 0; // If there's an error, treat them as equal - } - }; - case "byPDFTitle": - return (file1, file2) -> { - try (PDDocument doc1 = pdfDocumentFactory.load(file1); - PDDocument doc2 = pdfDocumentFactory.load(file2)) { - String title1 = doc1.getDocumentInformation().getTitle(); - String title2 = doc2.getDocumentInformation().getTitle(); - return title1.compareTo(title2); - } catch (IOException e) { - return 0; - } - }; - case "orderProvided": - default: - return (file1, file2) -> 0; // Default is the order provided - } + return switch (sortType) { + case "byFileName" -> + Comparator.comparing( + (MultipartFile mf) -> { + String name = mf.getOriginalFilename(); + return name == null ? "" : name; + }, + String.CASE_INSENSITIVE_ORDER); + case "byDateModified" -> + (file1, file2) -> { + long t1 = getPdfDateTimeSafe(file1); + long t2 = getPdfDateTimeSafe(file2); + return Long.compare(t2, t1); + }; + case "byDateCreated" -> + (file1, file2) -> { + long t1 = getPdfDateTimeSafe(file1); + long t2 = getPdfDateTimeSafe(file2); + return Long.compare(t2, t1); + }; + case "byPDFTitle" -> + (file1, file2) -> { + try (PDDocument doc1 = pdfDocumentFactory.load(file1); + PDDocument doc2 = pdfDocumentFactory.load(file2)) { + String title1 = + doc1.getDocumentInformation() != null + ? doc1.getDocumentInformation().getTitle() + : null; + String title2 = + doc2.getDocumentInformation() != null + ? doc2.getDocumentInformation().getTitle() + : null; + if (title1 == null && title2 == null) { + return 0; + } + if (title1 == null) { + return 1; + } + if (title2 == null) { + return -1; + } + return title1.compareToIgnoreCase(title2); + } catch (IOException e) { + return 0; + } + }; + case "orderProvided" -> (file1, file2) -> 0; // Default is the order provided + default -> (file1, file2) -> 0; // Default is the order provided + }; } // Parse client file IDs from JSON string @@ -152,10 +177,7 @@ public class MergeController { for (MultipartFile file : files) { // Get the filename without extension to use as bookmark title String filename = file.getOriginalFilename(); - String title = filename; - if (title != null && title.contains(".")) { - title = title.substring(0, title.lastIndexOf('.')); - } + String title = GeneralUtils.removeExtension(filename); // Create an outline item for this file PDOutlineItem item = new PDOutlineItem(); @@ -180,7 +202,56 @@ public class MergeController { } } - @AutoJobPostMapping(consumes = "multipart/form-data", value = "/merge-pdfs") + private long getPdfDateTimeSafe(MultipartFile file) { + try { + try (PDDocument doc = pdfDocumentFactory.load(file)) { + PDDocumentInformation info = doc.getDocumentInformation(); + if (info != null) { + if (info.getModificationDate() != null) { + return info.getModificationDate().getTimeInMillis(); + } + if (info.getCreationDate() != null) { + return info.getCreationDate().getTimeInMillis(); + } + } + + // Fallback to XMP metadata if Info dates are missing + PDMetadata metadata = doc.getDocumentCatalog().getMetadata(); + if (metadata != null) { + try (InputStream is = metadata.createInputStream()) { + DomXmpParser parser = new DomXmpParser(); + XMPMetadata xmp = parser.parse(is); + XMPBasicSchema basic = xmp.getXMPBasicSchema(); + if (basic != null) { + if (basic.getModifyDate() != null) { + return basic.getModifyDate().getTimeInMillis(); + } + if (basic.getCreateDate() != null) { + return basic.getCreateDate().getTimeInMillis(); + } + } + } catch (Exception e) { + log.debug( + "Unable to read XMP metadata dates from uploaded file: {}", + e.getMessage()); + } + } + } + } catch (IOException e) { + log.debug("Unable to read PDF dates from uploaded file: {}", e.getMessage()); + } + return 0L; + } + + private static int indexOfByOriginalFilename(List list, String name) { + for (int i = 0; i < list.size(); i++) { + MultipartFile f = list.get(i); + if (name.equals(f.getOriginalFilename())) return i; + } + return -1; + } + + @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/merge-pdfs") @StandardPdfResponse @Operation( summary = "Merge multiple PDF files into one", @@ -188,21 +259,34 @@ public class MergeController { "This endpoint merges multiple PDF files into a single PDF file. The merged" + " file will contain all pages from the input files in the order they were" + " provided. Input:PDF Output:PDF Type:MISO") - public ResponseEntity mergePdfs(@ModelAttribute MergePdfsRequest request) + public ResponseEntity mergePdfs( + @ModelAttribute MergePdfsRequest request, + @RequestParam(value = "fileOrder", required = false) String fileOrder) throws IOException { List filesToDelete = new ArrayList<>(); // List of temporary files to delete - File mergedTempFile = null; - PDDocument mergedDocument = null; + TempFile outputTempFile = null; boolean removeCertSign = Boolean.TRUE.equals(request.getRemoveCertSign()); boolean generateToc = request.isGenerateToc(); - try { - MultipartFile[] files = request.getFileInput(); + MultipartFile[] files = request.getFileInput(); + if (files == null) { + files = new MultipartFile[0]; + } + + // If front-end provided explicit visible order, honor it and override backend sorting + if (fileOrder != null && !fileOrder.isBlank()) { + files = reorderFilesByProvidedOrder(files, fileOrder); + } else { Arrays.sort( files, getSortComparator( - request.getSortType())); // Sort files based on the given sort type + request.getSortType())); // Sort files based on requested sort type + } + + ResponseEntity response; + + try (TempFile mt = new TempFile(tempFileManager, ".pdf")) { PDFMergerUtility mergerUtility = new PDFMergerUtility(); long totalSize = 0; @@ -211,7 +295,7 @@ public class MergeController { MultipartFile multipartFile = files[index]; totalSize += multipartFile.getSize(); File tempFile = - GeneralUtils.convertMultipartFileToFile( + tempFileManager.convertMultipartFileToFile( multipartFile); // Convert MultipartFile to File filesToDelete.add(tempFile); // Add temp file to the list for later deletion @@ -226,30 +310,7 @@ public class MergeController { mergerUtility.addSource(tempFile); // Add source file to the merger utility } - if (!invalidIndexes.isEmpty()) { - // Parse client file IDs (always present from frontend) - String[] clientIds = parseClientFileIds(request.getClientFileIds()); - - // Map invalid indexes to client IDs - List errorFileIds = new ArrayList<>(); - for (Integer index : invalidIndexes) { - if (index < clientIds.length) { - errorFileIds.add(clientIds[index]); - } - } - - String payload = - String.format( - "{\"errorFileIds\":%s,\"message\":\"Some of the selected files can't be merged\"}", - errorFileIds.toString()); - - return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY) - .header("Content-Type", MediaType.APPLICATION_JSON_VALUE) - .body(payload.getBytes(StandardCharsets.UTF_8)); - } - - mergedTempFile = Files.createTempFile("merged-", ".pdf").toFile(); - mergerUtility.setDestinationFileName(mergedTempFile.getAbsolutePath()); + mergerUtility.setDestinationFileName(mt.getFile().getAbsolutePath()); try { mergerUtility.mergeDocuments( @@ -263,42 +324,35 @@ public class MergeController { throw e; } - // Load the merged PDF document - mergedDocument = pdfDocumentFactory.load(mergedTempFile); + // Load the merged PDF document and operate on it inside try-with-resources + try (PDDocument mergedDocument = pdfDocumentFactory.load(mt.getFile())) { + // Remove signatures if removeCertSign is true + if (removeCertSign) { + PDDocumentCatalog catalog = mergedDocument.getDocumentCatalog(); + PDAcroForm acroForm = catalog.getAcroForm(); + if (acroForm != null) { + List fieldsToRemove = + acroForm.getFields().stream() + .filter(PDSignatureField.class::isInstance) + .toList(); - // Remove signatures if removeCertSign is true - if (removeCertSign) { - PDDocumentCatalog catalog = mergedDocument.getDocumentCatalog(); - PDAcroForm acroForm = catalog.getAcroForm(); - if (acroForm != null) { - List fieldsToRemove = - acroForm.getFields().stream() - .filter(field -> field instanceof PDSignatureField) - .toList(); - - if (!fieldsToRemove.isEmpty()) { - acroForm.flatten( - fieldsToRemove, - false); // Flatten the fields, effectively removing them + if (!fieldsToRemove.isEmpty()) { + acroForm.flatten( + fieldsToRemove, + false); // Flatten the fields, effectively removing them + } } } + + // Add table of contents if generateToc is true + if (generateToc && files.length > 0) { + addTableOfContents(mergedDocument, files); + } + + // Save the modified document to a temporary file + outputTempFile = new TempFile(tempFileManager, ".pdf"); + mergedDocument.save(outputTempFile.getFile()); } - - // Add table of contents if generateToc is true - if (generateToc && files.length > 0) { - addTableOfContents(mergedDocument, files); - } - - // Save the modified document to a new ByteArrayOutputStream - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - mergedDocument.save(baos); - - String mergedFileName = - files[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") - + "_merged_unsigned.pdf"; - return WebResponseUtils.baosToWebResponse( - baos, mergedFileName); // Return the modified PDF - } catch (Exception ex) { if (ex instanceof IOException && PdfErrorUtils.isCorruptedPdfError((IOException) ex)) { log.warn("Corrupted PDF detected in merge pdf process: {}", ex.getMessage()); @@ -307,17 +361,16 @@ public class MergeController { } throw ex; } finally { - if (mergedDocument != null) { - mergedDocument.close(); // Close the merged document - } for (File file : filesToDelete) { - if (file != null) { - Files.deleteIfExists(file.toPath()); // Delete temporary files - } - } - if (mergedTempFile != null) { - Files.deleteIfExists(mergedTempFile.toPath()); + tempFileManager.deleteTempFile(file); // Delete temporary files } } + + String firstFilename = files.length > 0 ? files[0].getOriginalFilename() : null; + String mergedFileName = + GeneralUtils.generateFilename(firstFilename, "_merged_unsigned.pdf"); + + response = WebResponseUtils.pdfFileToWebResponse(outputTempFile, mergedFileName); + return response; } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/MultiPageLayoutController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/MultiPageLayoutController.java index 631e50db6..40301c63e 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/MultiPageLayoutController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/MultiPageLayoutController.java @@ -1,6 +1,6 @@ package stirling.software.SPDF.controller.api; -import java.awt.*; +import java.awt.Color; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -11,29 +11,35 @@ import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; import org.apache.pdfbox.util.Matrix; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; -import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.config.swagger.StandardPdfResponse; import stirling.software.SPDF.model.api.general.MergeMultiplePagesRequest; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.GeneralApi; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.FormUtils; +import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.WebResponseUtils; @GeneralApi @RequiredArgsConstructor +@Slf4j public class MultiPageLayoutController { private final CustomPDFDocumentFactory pdfDocumentFactory; - @AutoJobPostMapping(value = "/multi-page-layout", consumes = "multipart/form-data") + @AutoJobPostMapping( + value = "/multi-page-layout", + consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @StandardPdfResponse @Operation( summary = "Merge multiple pages of a PDF document into a single page", @@ -100,7 +106,8 @@ public class MultiPageLayoutController { float scale = Math.min(scaleWidth, scaleHeight); int adjustedPageIndex = - i % pagesPerSheet; // This will reset the index for every new page + i % pagesPerSheet; // Close the current content stream and create a new + // page and content stream int rowIndex = adjustedPageIndex / cols; int colIndex = adjustedPageIndex % cols; @@ -128,7 +135,28 @@ public class MultiPageLayoutController { } } - contentStream.close(); // Close the final content stream + contentStream.close(); + + // If any source page is rotated, skip form copying/transformation entirely + boolean hasRotation = FormUtils.hasAnyRotatedPage(sourceDocument); + if (hasRotation) { + log.info("Source document has rotated pages; skipping form field copying."); + } else { + try { + FormUtils.copyAndTransformFormFields( + sourceDocument, + newDocument, + totalPages, + pagesPerSheet, + cols, + rows, + cellWidth, + cellHeight); + } catch (Exception e) { + log.warn("Failed to copy and transform form fields: {}", e.getMessage(), e); + } + } + sourceDocument.close(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); @@ -138,7 +166,7 @@ public class MultiPageLayoutController { byte[] result = baos.toByteArray(); return WebResponseUtils.bytesToWebResponse( result, - Filenames.toSimpleFileName(file.getOriginalFilename()).replaceFirst("[.][^.]+$", "") - + "_layoutChanged.pdf"); + GeneralUtils.generateFilename( + file.getOriginalFilename(), "_multi_page_layout.pdf")); } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/PdfImageRemovalController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/PdfImageRemovalController.java index 24d235282..9da42d057 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/PdfImageRemovalController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/PdfImageRemovalController.java @@ -4,6 +4,7 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import org.apache.pdfbox.pdmodel.PDDocument; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; @@ -17,6 +18,7 @@ import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.GeneralApi; import stirling.software.common.model.api.PDFFile; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.WebResponseUtils; /** @@ -43,7 +45,7 @@ public class PdfImageRemovalController { * content type and filename. * @throws IOException If an error occurs while processing the PDF file. */ - @AutoJobPostMapping(consumes = "multipart/form-data", value = "/remove-image-pdf") + @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/remove-image-pdf") @StandardPdfResponse @Operation( summary = "Remove images from file to reduce the file size.", @@ -66,8 +68,8 @@ public class PdfImageRemovalController { // Generate a new filename for the modified PDF String mergedFileName = - file.getFileInput().getOriginalFilename().replaceFirst("[.][^.]+$", "") - + "_removed_images.pdf"; + GeneralUtils.generateFilename( + file.getFileInput().getOriginalFilename(), "_images_removed.pdf"); // Convert the byte array to a web response and return it return WebResponseUtils.bytesToWebResponse(outputStream.toByteArray(), mergedFileName); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/PdfOverlayController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/PdfOverlayController.java index a71401a07..a4057bcd8 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/PdfOverlayController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/PdfOverlayController.java @@ -17,7 +17,6 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; -import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; @@ -36,7 +35,7 @@ public class PdfOverlayController { private final CustomPDFDocumentFactory pdfDocumentFactory; - @AutoJobPostMapping(value = "/overlay-pdfs", consumes = "multipart/form-data") + @AutoJobPostMapping(value = "/overlay-pdfs", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @StandardPdfResponse @Operation( summary = "Overlay PDF files in various modes", @@ -82,9 +81,8 @@ public class PdfOverlayController { overlay.overlay(overlayGuide).save(outputStream); byte[] data = outputStream.toByteArray(); String outputFilename = - Filenames.toSimpleFileName(baseFile.getOriginalFilename()) - .replaceFirst("[.][^.]+$", "") - + "_overlayed.pdf"; // Remove file extension and append .pdf + GeneralUtils.generateFilename( + baseFile.getOriginalFilename(), "_overlayed.pdf"); return WebResponseUtils.bytesToWebResponse( data, outputFilename, MediaType.APPLICATION_PDF); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/RearrangePagesPDFController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/RearrangePagesPDFController.java index adeddc743..7f725918f 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/RearrangePagesPDFController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/RearrangePagesPDFController.java @@ -7,11 +7,11 @@ import java.util.List; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; -import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; @@ -35,7 +35,7 @@ public class RearrangePagesPDFController { private final CustomPDFDocumentFactory pdfDocumentFactory; - @AutoJobPostMapping(consumes = "multipart/form-data", value = "/remove-pages") + @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/remove-pages") @StandardPdfResponse @Operation( summary = "Remove pages from a PDF file", @@ -65,9 +65,7 @@ public class RearrangePagesPDFController { } return WebResponseUtils.pdfDocToWebResponse( document, - Filenames.toSimpleFileName(pdfFile.getOriginalFilename()) - .replaceFirst("[.][^.]+$", "") - + "_removed_pages.pdf"); + GeneralUtils.generateFilename(pdfFile.getOriginalFilename(), "_removed_pages.pdf")); } private List removeFirst(int totalPages) { @@ -205,37 +203,26 @@ public class RearrangePagesPDFController { private List processSortTypes(String sortTypes, int totalPages, String pageOrder) { try { SortTypes mode = SortTypes.valueOf(sortTypes.toUpperCase()); - switch (mode) { - case REVERSE_ORDER: - return reverseOrder(totalPages); - case DUPLEX_SORT: - return duplexSort(totalPages); - case BOOKLET_SORT: - return bookletSort(totalPages); - case SIDE_STITCH_BOOKLET_SORT: - return sideStitchBooklet(totalPages); - case ODD_EVEN_SPLIT: - return oddEvenSplit(totalPages); - case ODD_EVEN_MERGE: - return oddEvenMerge(totalPages); - case REMOVE_FIRST: - return removeFirst(totalPages); - case REMOVE_LAST: - return removeLast(totalPages); - case REMOVE_FIRST_AND_LAST: - return removeFirstAndLast(totalPages); - case DUPLICATE: - return duplicate(totalPages, pageOrder); - default: - throw new IllegalArgumentException("Unsupported custom mode"); - } + return switch (mode) { + case REVERSE_ORDER -> reverseOrder(totalPages); + case DUPLEX_SORT -> duplexSort(totalPages); + case BOOKLET_SORT -> bookletSort(totalPages); + case SIDE_STITCH_BOOKLET_SORT -> sideStitchBooklet(totalPages); + case ODD_EVEN_SPLIT -> oddEvenSplit(totalPages); + case ODD_EVEN_MERGE -> oddEvenMerge(totalPages); + case REMOVE_FIRST -> removeFirst(totalPages); + case REMOVE_LAST -> removeLast(totalPages); + case REMOVE_FIRST_AND_LAST -> removeFirstAndLast(totalPages); + case DUPLICATE -> duplicate(totalPages, pageOrder); + default -> throw new IllegalArgumentException("Unsupported custom mode"); + }; } catch (IllegalArgumentException e) { log.error("Unsupported custom mode", e); return null; } } - @AutoJobPostMapping(consumes = "multipart/form-data", value = "/rearrange-pages") + @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/rearrange-pages") @StandardPdfResponse @Operation( summary = "Rearrange pages in a PDF file", @@ -258,14 +245,14 @@ public class RearrangePagesPDFController { int totalPages = document.getNumberOfPages(); List newPageOrder; if (sortType != null - && sortType.length() > 0 + && !sortType.isEmpty() && !"custom".equals(sortType.toLowerCase())) { newPageOrder = processSortTypes(sortType, totalPages, pageOrder); } else { newPageOrder = GeneralUtils.parsePageList(pageOrderArr, totalPages, false); } - log.info("newPageOrder = " + newPageOrder); - log.info("totalPages = " + totalPages); + log.info("newPageOrder = {}", newPageOrder); + log.info("totalPages = {}", totalPages); // Create a new list to hold the pages in the new order List newPages = new ArrayList<>(); for (int i = 0; i < newPageOrder.size(); i++) { @@ -284,9 +271,8 @@ public class RearrangePagesPDFController { return WebResponseUtils.pdfDocToWebResponse( document, - Filenames.toSimpleFileName(pdfFile.getOriginalFilename()) - .replaceFirst("[.][^.]+$", "") - + "_rearranged.pdf"); + GeneralUtils.generateFilename( + pdfFile.getOriginalFilename(), "_rearranged.pdf")); } catch (IOException e) { ExceptionUtils.logException("document rearrangement", e); throw e; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/RotationController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/RotationController.java index 0fc4c9324..25437b432 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/RotationController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/RotationController.java @@ -5,11 +5,11 @@ import java.io.IOException; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPageTree; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; -import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; @@ -20,6 +20,7 @@ import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.GeneralApi; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; +import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.WebResponseUtils; @GeneralApi @@ -28,7 +29,7 @@ public class RotationController { private final CustomPDFDocumentFactory pdfDocumentFactory; - @AutoJobPostMapping(consumes = "multipart/form-data", value = "/rotate-pdf") + @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/rotate-pdf") @StandardPdfResponse @Operation( summary = "Rotate a PDF file", @@ -56,10 +57,9 @@ public class RotationController { page.setRotation(page.getRotation() + angle); } + // Return the rotated PDF as a response return WebResponseUtils.pdfDocToWebResponse( document, - Filenames.toSimpleFileName(pdfFile.getOriginalFilename()) - .replaceFirst("[.][^.]+$", "") - + "_rotated.pdf"); + GeneralUtils.generateFilename(pdfFile.getOriginalFilename(), "_rotated.pdf")); } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/ScalePagesController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/ScalePagesController.java index 2fad9d7cc..2c854d4e5 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/ScalePagesController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/ScalePagesController.java @@ -12,11 +12,11 @@ import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; import org.apache.pdfbox.util.Matrix; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; -import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; @@ -27,6 +27,7 @@ import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.GeneralApi; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; +import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.WebResponseUtils; @GeneralApi @@ -35,7 +36,7 @@ public class ScalePagesController { private final CustomPDFDocumentFactory pdfDocumentFactory; - @AutoJobPostMapping(value = "/scale-pages", consumes = "multipart/form-data") + @AutoJobPostMapping(value = "/scale-pages", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @StandardPdfResponse @Operation( summary = "Change the size of a PDF page/document", @@ -96,20 +97,26 @@ public class ScalePagesController { return WebResponseUtils.bytesToWebResponse( baos.toByteArray(), - Filenames.toSimpleFileName(file.getOriginalFilename()).replaceFirst("[.][^.]+$", "") - + "_scaled.pdf"); + GeneralUtils.generateFilename(file.getOriginalFilename(), "_scaled.pdf")); } private PDRectangle getTargetSize(String targetPDRectangle, PDDocument sourceDocument) { if ("KEEP".equals(targetPDRectangle)) { if (sourceDocument.getNumberOfPages() == 0) { - return null; + // Do not return null here; throw a clear exception so callers don't get a nullable + // PDRectangle. + throw ExceptionUtils.createInvalidPageSizeException("KEEP"); } // use the first page to determine the target page size PDPage sourcePage = sourceDocument.getPage(0); PDRectangle sourceSize = sourcePage.getMediaBox(); + if (sourceSize == null) { + // If media box is unexpectedly null, treat it as invalid + throw ExceptionUtils.createInvalidPageSizeException("KEEP"); + } + return sourceSize; } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPDFController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPDFController.java index cb92cffac..e20ac0128 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPDFController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPDFController.java @@ -3,7 +3,6 @@ package stirling.software.SPDF.controller.api; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.file.Files; -import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; @@ -17,7 +16,6 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; -import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; @@ -29,6 +27,9 @@ import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.GeneralApi; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; +import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @GeneralApi @@ -37,8 +38,9 @@ import stirling.software.common.util.WebResponseUtils; public class SplitPDFController { private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; - @AutoJobPostMapping(consumes = "multipart/form-data", value = "/split-pages") + @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/split-pages") @MultiFileResponse @Operation( summary = "Split a PDF file into separate documents", @@ -51,17 +53,15 @@ public class SplitPDFController { throws IOException { PDDocument document = null; - Path zipFile = null; List splitDocumentsBoas = new ArrayList<>(); + TempFile outputTempFile = null; try { + outputTempFile = new TempFile(tempFileManager, ".zip"); MultipartFile file = request.getFileInput(); - String pages = request.getPageNumbers(); - // open the pdf document - document = pdfDocumentFactory.load(file); - // PdfMetadata metadata = PdfMetadataService.extractMetadataFromPdf(document); + int totalPages = document.getNumberOfPages(); List pageNumbers = request.getPageNumbersList(document, false); if (!pageNumbers.contains(totalPages - 1)) { @@ -74,8 +74,7 @@ public class SplitPDFController { "Splitting PDF into pages: {}", pageNumbers.stream().map(String::valueOf).collect(Collectors.joining(","))); - // split the document - splitDocumentsBoas = new ArrayList<>(); + splitDocumentsBoas = new ArrayList<>(pageNumbers.size()); int previousPageNumber = 0; for (int splitPoint : pageNumbers) { try (PDDocument splitDocument = @@ -92,7 +91,6 @@ public class SplitPDFController { ByteArrayOutputStream baos = new ByteArrayOutputStream(); splitDocument.save(baos); - splitDocumentsBoas.add(baos); } catch (Exception e) { ExceptionUtils.logException("document splitting and saving", e); @@ -100,22 +98,21 @@ public class SplitPDFController { } } - // closing the original document document.close(); - zipFile = Files.createTempFile("split_documents", ".zip"); + String baseFilename = GeneralUtils.removeExtension(file.getOriginalFilename()); + + try (ZipOutputStream zipOut = + new ZipOutputStream(Files.newOutputStream(outputTempFile.getPath()))) { + int splitDocumentsSize = splitDocumentsBoas.size(); + for (int i = 0; i < splitDocumentsSize; i++) { + StringBuilder sb = new StringBuilder(baseFilename.length() + 10); + sb.append(baseFilename).append('_').append(i + 1).append(".pdf"); + String fileName = sb.toString(); - String filename = - Filenames.toSimpleFileName(file.getOriginalFilename()) - .replaceFirst("[.][^.]+$", ""); - try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile))) { - // loop through the split documents and write them to the zip file - for (int i = 0; i < splitDocumentsBoas.size(); i++) { - String fileName = filename + "_" + (i + 1) + ".pdf"; ByteArrayOutputStream baos = splitDocumentsBoas.get(i); byte[] pdf = baos.toByteArray(); - // Add PDF file to the zip ZipEntry pdfEntry = new ZipEntry(fileName); zipOut.putNextEntry(pdfEntry); zipOut.write(pdf); @@ -123,18 +120,17 @@ public class SplitPDFController { log.debug("Wrote split document {} to zip file", fileName); } - } catch (Exception e) { - log.error("Failed writing to zip", e); - throw e; } - log.debug("Successfully created zip file with split documents: {}", zipFile.toString()); - byte[] data = Files.readAllBytes(zipFile); - Files.deleteIfExists(zipFile); + log.debug( + "Successfully created zip file with split documents: {}", + outputTempFile.getPath().toString()); + byte[] data = Files.readAllBytes(outputTempFile.getPath()); - // return the Resource in the response + String zipFilename = + GeneralUtils.generateFilename(file.getOriginalFilename(), "_split.zip"); return WebResponseUtils.bytesToWebResponse( - data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM); + data, zipFilename, MediaType.APPLICATION_OCTET_STREAM); } finally { try { @@ -150,9 +146,9 @@ public class SplitPDFController { } } - // Delete temporary zip file - if (zipFile != null) { - Files.deleteIfExists(zipFile); + // Close the output temporary file + if (outputTempFile != null) { + outputTempFile.close(); } } catch (Exception e) { log.error("Error while cleaning up resources", e); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPdfByChaptersController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPdfByChaptersController.java index c58003d2a..c17d6dcf1 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPdfByChaptersController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPdfByChaptersController.java @@ -17,7 +17,6 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; -import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; import lombok.AllArgsConstructor; @@ -35,6 +34,7 @@ import stirling.software.common.model.PdfMetadata; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.service.PdfMetadataService; import stirling.software.common.util.ExceptionUtils; +import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.WebResponseUtils; @GeneralApi @@ -114,7 +114,9 @@ public class SplitPdfByChaptersController { return bookmarks; } - @AutoJobPostMapping(value = "/split-pdf-by-chapters", consumes = "multipart/form-data") + @AutoJobPostMapping( + value = "/split-pdf-by-chapters", + consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @MultiFileResponse @Operation( summary = "Split PDFs by Chapters", @@ -186,9 +188,7 @@ public class SplitPdfByChaptersController { byte[] data = Files.readAllBytes(zipFile); Files.deleteIfExists(zipFile); - String filename = - Filenames.toSimpleFileName(file.getOriginalFilename()) - .replaceFirst("[.][^.]+$", ""); + String filename = GeneralUtils.generateFilename(file.getOriginalFilename(), ""); sourceDocument.close(); return WebResponseUtils.bytesToWebResponse( data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySectionsController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySectionsController.java index 8835bbb74..2b2253f86 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySectionsController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySectionsController.java @@ -2,8 +2,8 @@ package stirling.software.SPDF.controller.api; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.OutputStream; import java.nio.file.Files; -import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.zip.ZipEntry; @@ -21,8 +21,8 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; -import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; @@ -32,6 +32,10 @@ import stirling.software.SPDF.model.api.SplitPdfBySectionsRequest; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.GeneralApi; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.PDFService; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @GeneralApi @@ -39,8 +43,12 @@ import stirling.software.common.util.WebResponseUtils; public class SplitPdfBySectionsController { private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; + private final PDFService pdfService; - @AutoJobPostMapping(value = "/split-pdf-by-sections", consumes = "multipart/form-data") + @AutoJobPostMapping( + value = "/split-pdf-by-sections", + consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @MultiFileResponse @Operation( summary = "Split PDF pages into smaller sections", @@ -48,8 +56,8 @@ public class SplitPdfBySectionsController { "Split each page of a PDF into smaller sections based on the user's choice" + " (halves, thirds, quarters, etc.), both vertically and horizontally." + " Input:PDF Output:ZIP-PDF Type:SISO") - public ResponseEntity splitPdf(@ModelAttribute SplitPdfBySectionsRequest request) - throws Exception { + public ResponseEntity splitPdf( + @ModelAttribute SplitPdfBySectionsRequest request) throws Exception { List splitDocumentsBoas = new ArrayList<>(); MultipartFile file = request.getFileInput(); @@ -61,14 +69,16 @@ public class SplitPdfBySectionsController { boolean merge = Boolean.TRUE.equals(request.getMerge()); List splitDocuments = splitPdfPages(sourceDocument, verti, horiz); - String filename = - Filenames.toSimpleFileName(file.getOriginalFilename()) - .replaceFirst("[.][^.]+$", ""); + String filename = GeneralUtils.generateFilename(file.getOriginalFilename(), "_split.pdf"); if (merge) { - MergeController mergeController = new MergeController(pdfDocumentFactory); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - mergeController.mergeDocuments(splitDocuments).save(baos); - return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), filename + "_split.pdf"); + TempFile tempFile = new TempFile(tempFileManager, ".pdf"); + try (PDDocument merged = pdfService.mergeDocuments(splitDocuments); + OutputStream out = Files.newOutputStream(tempFile.getPath())) { + merged.save(out); + for (PDDocument d : splitDocuments) d.close(); + sourceDocument.close(); + } + return WebResponseUtils.pdfFileToWebResponse(tempFile, filename + "_split.pdf"); } for (PDDocument doc : splitDocuments) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); @@ -79,10 +89,9 @@ public class SplitPdfBySectionsController { sourceDocument.close(); - Path zipFile = Files.createTempFile("split_documents", ".zip"); - byte[] data; - - try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile))) { + TempFile zipTempFile = new TempFile(tempFileManager, ".zip"); + try (ZipOutputStream zipOut = + new ZipOutputStream(Files.newOutputStream(zipTempFile.getPath()))) { int pageNum = 1; for (int i = 0; i < splitDocumentsBoas.size(); i++) { ByteArrayOutputStream baos = splitDocumentsBoas.get(i); @@ -96,15 +105,8 @@ public class SplitPdfBySectionsController { if (sectionNum == horiz * verti) pageNum++; } - - zipOut.finish(); - data = Files.readAllBytes(zipFile); - return WebResponseUtils.bytesToWebResponse( - data, filename + "_split.zip", MediaType.APPLICATION_OCTET_STREAM); - - } finally { - Files.deleteIfExists(zipFile); } + return WebResponseUtils.zipFileToWebResponse(zipTempFile, filename + "_split.zip"); } public List splitPdfPages( diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySizeController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySizeController.java index 955ccf2ad..8de9dac0d 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySizeController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySizeController.java @@ -14,7 +14,6 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; -import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; @@ -27,6 +26,8 @@ import stirling.software.common.annotations.api.GeneralApi; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @GeneralApi @@ -35,8 +36,11 @@ import stirling.software.common.util.WebResponseUtils; public class SplitPdfBySizeController { private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; - @AutoJobPostMapping(value = "/split-by-size-or-count", consumes = "multipart/form-data") + @AutoJobPostMapping( + value = "/split-by-size-or-count", + consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @MultiFileResponse @Operation( summary = "Auto split PDF pages into separate documents based on size or count", @@ -52,89 +56,67 @@ public class SplitPdfBySizeController { log.debug("Starting PDF split process with request: {}", request); MultipartFile file = request.getFileInput(); - Path zipFile = Files.createTempFile("split_documents", ".zip"); - log.debug("Created temporary zip file: {}", zipFile); - - String filename = - Filenames.toSimpleFileName(file.getOriginalFilename()) - .replaceFirst("[.][^.]+$", ""); + String filename = GeneralUtils.generateFilename(file.getOriginalFilename(), ""); log.debug("Base filename for output: {}", filename); - byte[] data = null; - try { - log.debug("Reading input file bytes"); - byte[] pdfBytes = file.getBytes(); - log.debug("Successfully read {} bytes from input file", pdfBytes.length); + try (TempFile zipTempFile = new TempFile(tempFileManager, ".zip")) { + Path managedZipPath = zipTempFile.getPath(); + log.debug("Created temporary managed zip file: {}", managedZipPath); + try { + log.debug("Reading input file bytes"); + byte[] pdfBytes = file.getBytes(); + log.debug("Successfully read {} bytes from input file", pdfBytes.length); - log.debug("Creating ZIP output stream"); - try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile))) { - log.debug("Loading PDF document"); - try (PDDocument sourceDocument = pdfDocumentFactory.load(pdfBytes)) { - log.debug( - "Successfully loaded PDF with {} pages", - sourceDocument.getNumberOfPages()); + log.debug("Creating ZIP output stream"); + try (ZipOutputStream zipOut = + new ZipOutputStream(Files.newOutputStream(managedZipPath))) { + log.debug("Loading PDF document"); + try (PDDocument sourceDocument = pdfDocumentFactory.load(pdfBytes)) { + log.debug( + "Successfully loaded PDF with {} pages", + sourceDocument.getNumberOfPages()); - int type = request.getSplitType(); - String value = request.getSplitValue(); - log.debug("Split type: {}, Split value: {}", type, value); + int type = request.getSplitType(); + String value = request.getSplitValue(); + log.debug("Split type: {}, Split value: {}", type, value); - if (type == 0) { - log.debug("Processing split by size"); - long maxBytes = GeneralUtils.convertSizeToBytes(value); - log.debug("Max bytes per document: {}", maxBytes); - handleSplitBySize(sourceDocument, maxBytes, zipOut, filename); - } else if (type == 1) { - log.debug("Processing split by page count"); - int pageCount = Integer.parseInt(value); - log.debug("Pages per document: {}", pageCount); - handleSplitByPageCount(sourceDocument, pageCount, zipOut, filename); - } else if (type == 2) { - log.debug("Processing split by document count"); - int documentCount = Integer.parseInt(value); - log.debug("Total number of documents: {}", documentCount); - handleSplitByDocCount(sourceDocument, documentCount, zipOut, filename); - } else { - log.error("Invalid split type: {}", type); - throw ExceptionUtils.createIllegalArgumentException( - "error.invalidArgument", - "Invalid argument: {0}", - "split type: " + type); + if (type == 0) { + log.debug("Processing split by size"); + long maxBytes = GeneralUtils.convertSizeToBytes(value); + log.debug("Max bytes per document: {}", maxBytes); + handleSplitBySize(sourceDocument, maxBytes, zipOut, filename); + } else if (type == 1) { + log.debug("Processing split by page count"); + int pageCount = Integer.parseInt(value); + log.debug("Pages per document: {}", pageCount); + handleSplitByPageCount(sourceDocument, pageCount, zipOut, filename); + } else if (type == 2) { + log.debug("Processing split by document count"); + int documentCount = Integer.parseInt(value); + log.debug("Total number of documents: {}", documentCount); + handleSplitByDocCount(sourceDocument, documentCount, zipOut, filename); + } else { + log.error("Invalid split type: {}", type); + throw ExceptionUtils.createIllegalArgumentException( + "error.invalidArgument", + "Invalid argument: {0}", + "split type: " + type); + } + log.debug("PDF splitting completed successfully"); } - - log.debug("PDF splitting completed successfully"); - } catch (Exception e) { - ExceptionUtils.logException("PDF document loading or processing", e); - throw e; } - } catch (IOException e) { - log.error("Error creating or writing to ZIP file", e); - throw e; - } - } catch (Exception e) { - ExceptionUtils.logException("PDF splitting process", e); - throw e; // Re-throw to ensure proper error response - } finally { - try { - log.debug("Reading ZIP file data"); - data = Files.readAllBytes(zipFile); + byte[] data = Files.readAllBytes(managedZipPath); log.debug("Successfully read {} bytes from ZIP file", data.length); - } catch (IOException e) { - log.error("Error reading ZIP file data", e); - } - try { - log.debug("Deleting temporary ZIP file"); - boolean deleted = Files.deleteIfExists(zipFile); - log.debug("Temporary ZIP file deleted: {}", deleted); - } catch (IOException e) { - log.error("Error deleting temporary ZIP file", e); + log.debug("Returning response with {} bytes of data", data.length); + return WebResponseUtils.bytesToWebResponse( + data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM); + } catch (Exception e) { + ExceptionUtils.logException("PDF splitting process", e); + throw e; // Re-throw to ensure proper error response } } - - log.debug("Returning response with {} bytes of data", data != null ? data.length : 0); - return WebResponseUtils.bytesToWebResponse( - data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM); } private void handleSplitBySize( @@ -271,7 +253,7 @@ public class SplitPdfBySizeController { log.debug("Starting handleSplitByPageCount with pageCount={}", pageCount); int currentPageCount = 0; log.debug("Creating initial output document"); - PDDocument currentDoc = null; + PDDocument currentDoc; try { currentDoc = pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument); log.debug("Successfully created initial output document"); @@ -385,7 +367,7 @@ public class SplitPdfBySizeController { for (int i = 0; i < documentCount; i++) { log.debug("Creating document {} of {}", i + 1, documentCount); - PDDocument currentDoc = null; + PDDocument currentDoc; try { currentDoc = pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument); log.debug("Successfully created document {} of {}", i + 1, documentCount); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/ToSinglePageController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/ToSinglePageController.java index 1350d3f77..fcf858f67 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/ToSinglePageController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/ToSinglePageController.java @@ -10,6 +10,7 @@ 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.form.PDFormXObject; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; @@ -22,6 +23,7 @@ import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.GeneralApi; import stirling.software.common.model.api.PDFFile; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.WebResponseUtils; @GeneralApi @@ -30,7 +32,9 @@ public class ToSinglePageController { private final CustomPDFDocumentFactory pdfDocumentFactory; - @AutoJobPostMapping(consumes = "multipart/form-data", value = "/pdf-to-single-page") + @AutoJobPostMapping( + consumes = MediaType.MULTIPART_FORM_DATA_VALUE, + value = "/pdf-to-single-page") @StandardPdfResponse @Operation( summary = "Convert a multi-page PDF into a single long page PDF", @@ -89,7 +93,7 @@ public class ToSinglePageController { byte[] result = baos.toByteArray(); return WebResponseUtils.bytesToWebResponse( result, - request.getFileInput().getOriginalFilename().replaceFirst("[.][^.]+$", "") - + "_singlePage.pdf"); + GeneralUtils.generateFilename( + request.getFileInput().getOriginalFilename(), "_singlePage.pdf")); } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertEmlToPDF.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertEmlToPDF.java index c4f4d4305..f043cf057 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertEmlToPDF.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertEmlToPDF.java @@ -37,7 +37,7 @@ public class ConvertEmlToPDF { private final TempFileManager tempFileManager; private final CustomHtmlSanitizer customHtmlSanitizer; - @AutoJobPostMapping(consumes = "multipart/form-data", value = "/eml/pdf") + @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/eml/pdf") @StandardPdfResponse @Operation( summary = "Convert EML to PDF", diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertHtmlToPDF.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertHtmlToPDF.java index f9f380dbc..85ef34313 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertHtmlToPDF.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertHtmlToPDF.java @@ -1,5 +1,6 @@ package stirling.software.SPDF.controller.api.converters; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; @@ -15,11 +16,7 @@ import stirling.software.common.annotations.api.ConvertApi; import stirling.software.common.configuration.RuntimePathConfig; import stirling.software.common.model.api.converters.HTMLToPdfRequest; import stirling.software.common.service.CustomPDFDocumentFactory; -import stirling.software.common.util.CustomHtmlSanitizer; -import stirling.software.common.util.ExceptionUtils; -import stirling.software.common.util.FileToPdf; -import stirling.software.common.util.TempFileManager; -import stirling.software.common.util.WebResponseUtils; +import stirling.software.common.util.*; @ConvertApi @RequiredArgsConstructor @@ -33,7 +30,7 @@ public class ConvertHtmlToPDF { private final CustomHtmlSanitizer customHtmlSanitizer; - @AutoJobPostMapping(consumes = "multipart/form-data", value = "/html/pdf") + @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/html/pdf") @StandardPdfResponse @Operation( summary = "Convert an HTML or ZIP (containing HTML and CSS) to PDF", @@ -66,9 +63,7 @@ public class ConvertHtmlToPDF { pdfBytes = pdfDocumentFactory.createNewBytesBasedOnOldDocument(pdfBytes); - String outputFilename = - originalFilename.replaceFirst("[.][^.]+$", "") - + ".pdf"; // Remove file extension and append .pdf + String outputFilename = GeneralUtils.generateFilename(originalFilename, ".pdf"); return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename); } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertImgPDFController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertImgPDFController.java index db09833bf..1174ff259 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertImgPDFController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertImgPDFController.java @@ -1,7 +1,6 @@ package stirling.software.SPDF.controller.api.converters; import java.io.ByteArrayOutputStream; -import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.net.URLConnection; @@ -9,6 +8,9 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; +import java.util.stream.Stream; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @@ -16,30 +18,41 @@ import org.apache.commons.io.FileUtils; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.rendering.ImageType; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; -import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import stirling.software.SPDF.config.EndpointConfiguration; import stirling.software.SPDF.config.swagger.MultiFileResponse; import stirling.software.SPDF.config.swagger.StandardPdfResponse; +import stirling.software.SPDF.model.api.converters.ConvertCbrToPdfRequest; +import stirling.software.SPDF.model.api.converters.ConvertCbzToPdfRequest; +import stirling.software.SPDF.model.api.converters.ConvertPdfToCbrRequest; +import stirling.software.SPDF.model.api.converters.ConvertPdfToCbzRequest; import stirling.software.SPDF.model.api.converters.ConvertToImageRequest; import stirling.software.SPDF.model.api.converters.ConvertToPdfRequest; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.ConvertApi; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.CbrUtils; +import stirling.software.common.util.CbzUtils; import stirling.software.common.util.CheckProgramInstall; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.PdfToCbrUtils; +import stirling.software.common.util.PdfToCbzUtils; import stirling.software.common.util.PdfUtils; import stirling.software.common.util.ProcessExecutor; import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult; +import stirling.software.common.util.RegexPatternUtils; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @ConvertApi @@ -48,8 +61,17 @@ import stirling.software.common.util.WebResponseUtils; public class ConvertImgPDFController { private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; + private final EndpointConfiguration endpointConfiguration; + private static final Pattern EXTENSION_PATTERN = + RegexPatternUtils.getInstance().getPattern(RegexPatternUtils.getExtensionRegex()); + private static final String DEFAULT_COMIC_NAME = "comic"; - @AutoJobPostMapping(consumes = "multipart/form-data", value = "/pdf/img") + private boolean isGhostscriptEnabled() { + return endpointConfiguration.isGroupEnabled("Ghostscript"); + } + + @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/img") @MultiFileResponse @Operation( summary = "Convert PDF to image(s)", @@ -65,6 +87,7 @@ public class ConvertImgPDFController { String colorType = request.getColorType(); int dpi = request.getDpi(); String pageNumbers = request.getPageNumbers(); + boolean includeAnnotations = Boolean.TRUE.equals(request.getIncludeAnnotations()); Path tempFile = null; Path tempOutputDir = null; Path tempPdfPath = null; @@ -86,9 +109,7 @@ public class ConvertImgPDFController { } // returns bytes for image boolean singleImage = "single".equals(singleOrMultiple); - String filename = - Filenames.toSimpleFileName(new File(file.getOriginalFilename()).getName()) - .replaceFirst("[.][^.]+$", ""); + String filename = GeneralUtils.generateFilename(file.getOriginalFilename(), ""); result = PdfUtils.convertFromPdf( @@ -100,7 +121,8 @@ public class ConvertImgPDFController { colorTypeResult, singleImage, dpi, - filename); + filename, + includeAnnotations); if (result == null || result.length == 0) { log.error("resultant bytes for {} is null, error converting ", filename); } @@ -147,10 +169,11 @@ public class ConvertImgPDFController { .runCommandWithOutputHandling(command); // Find all WebP files in the output directory - List webpFiles = - Files.walk(tempOutputDir) - .filter(path -> path.toString().endsWith(".webp")) - .toList(); + List webpFiles; + try (Stream walkStream = Files.walk(tempOutputDir)) { + webpFiles = + walkStream.filter(path -> path.toString().endsWith(".webp")).toList(); + } if (webpFiles.isEmpty()) { log.error("No WebP files were created in: {}", tempOutputDir.toString()); @@ -210,7 +233,7 @@ public class ConvertImgPDFController { } } - @AutoJobPostMapping(consumes = "multipart/form-data", value = "/img/pdf") + @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/img/pdf") @StandardPdfResponse @Operation( summary = "Convert images to a PDF file", @@ -236,13 +259,163 @@ public class ConvertImgPDFController { PdfUtils.imageToPdf(file, fitOption, autoRotate, colorType, pdfDocumentFactory); return WebResponseUtils.bytesToWebResponse( bytes, - new File(file[0].getOriginalFilename()).getName().replaceFirst("[.][^.]+$", "") - + "_converted.pdf"); + GeneralUtils.generateFilename(file[0].getOriginalFilename(), "_converted.pdf")); + } + + @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/cbz/pdf") + @Operation( + summary = "Convert CBZ comic book archive to PDF", + description = + "This endpoint converts a CBZ (ZIP) comic book archive to a PDF file. " + + "Input:CBZ Output:PDF Type:SISO") + public ResponseEntity convertCbzToPdf(@ModelAttribute ConvertCbzToPdfRequest request) + throws IOException { + MultipartFile file = request.getFileInput(); + boolean optimizeForEbook = request.isOptimizeForEbook(); + + // Disable optimization if Ghostscript is not available + if (optimizeForEbook && !isGhostscriptEnabled()) { + log.warn("Ghostscript optimization requested but Ghostscript is not enabled/available"); + optimizeForEbook = false; + } + + byte[] pdfBytes; + try { + pdfBytes = + CbzUtils.convertCbzToPdf( + file, pdfDocumentFactory, tempFileManager, optimizeForEbook); + } catch (IllegalArgumentException ex) { + String message = ex.getMessage() == null ? "Invalid CBZ file" : ex.getMessage(); + Map errorBody = + Map.of("error", "Invalid CBZ file", "message", message, "trace", ""); + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .contentType(MediaType.APPLICATION_JSON) + .body(errorBody); + } + + String filename = createConvertedFilename(file.getOriginalFilename(), "_converted.pdf"); + + return WebResponseUtils.bytesToWebResponse(pdfBytes, filename); + } + + @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/cbz") + @Operation( + summary = "Convert PDF to CBZ comic book archive", + description = + "This endpoint converts a PDF file to a CBZ (ZIP) comic book archive. " + + "Input:PDF Output:CBZ Type:SISO") + public ResponseEntity convertPdfToCbz(@ModelAttribute ConvertPdfToCbzRequest request) + throws IOException { + MultipartFile file = request.getFileInput(); + int dpi = request.getDpi(); + + if (dpi <= 0) { + dpi = 300; + } + + byte[] cbzBytes; + try { + cbzBytes = PdfToCbzUtils.convertPdfToCbz(file, dpi, pdfDocumentFactory); + } catch (IllegalArgumentException ex) { + String message = ex.getMessage() == null ? "Invalid PDF file" : ex.getMessage(); + Map errorBody = + Map.of("error", "Invalid PDF file", "message", message, "trace", ""); + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .contentType(MediaType.APPLICATION_JSON) + .body(errorBody); + } + + String filename = createConvertedFilename(file.getOriginalFilename(), "_converted.cbz"); + + return WebResponseUtils.bytesToWebResponse( + cbzBytes, filename, MediaType.APPLICATION_OCTET_STREAM); + } + + @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/cbr/pdf") + @Operation( + summary = "Convert CBR comic book archive to PDF", + description = + "This endpoint converts a CBR (RAR) comic book archive to a PDF file. " + + "Input:CBR Output:PDF Type:SISO") + public ResponseEntity convertCbrToPdf(@ModelAttribute ConvertCbrToPdfRequest request) + throws IOException { + MultipartFile file = request.getFileInput(); + boolean optimizeForEbook = request.isOptimizeForEbook(); + + // Disable optimization if Ghostscript is not available + if (optimizeForEbook && !isGhostscriptEnabled()) { + log.warn("Ghostscript optimization requested but Ghostscript is not enabled/available"); + optimizeForEbook = false; + } + + byte[] pdfBytes; + try { + pdfBytes = + CbrUtils.convertCbrToPdf( + file, pdfDocumentFactory, tempFileManager, optimizeForEbook); + } catch (IllegalArgumentException ex) { + String message = ex.getMessage() == null ? "Invalid CBR file" : ex.getMessage(); + Map errorBody = + Map.of("error", "Invalid CBR file", "message", message, "trace", ""); + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .contentType(MediaType.APPLICATION_JSON) + .body(errorBody); + } + + String filename = createConvertedFilename(file.getOriginalFilename(), "_converted.pdf"); + + return WebResponseUtils.bytesToWebResponse(pdfBytes, filename); + } + + @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/cbr") + @Operation( + summary = "Convert PDF to CBR comic book archive", + description = + "This endpoint converts a PDF file to a CBR comic book archive using the local RAR CLI. " + + "Input:PDF Output:CBR Type:SISO") + public ResponseEntity convertPdfToCbr(@ModelAttribute ConvertPdfToCbrRequest request) + throws IOException { + MultipartFile file = request.getFileInput(); + int dpi = request.getDpi(); + + if (dpi <= 0) { + dpi = 300; + } + + byte[] cbrBytes; + try { + cbrBytes = PdfToCbrUtils.convertPdfToCbr(file, dpi, pdfDocumentFactory); + } catch (IllegalArgumentException ex) { + String message = ex.getMessage() == null ? "Invalid PDF file" : ex.getMessage(); + Map errorBody = + Map.of("error", "Invalid PDF file", "message", message, "trace", ""); + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .contentType(MediaType.APPLICATION_JSON) + .body(errorBody); + } + + String filename = createConvertedFilename(file.getOriginalFilename(), "_converted.cbr"); + + return WebResponseUtils.bytesToWebResponse( + cbrBytes, filename, MediaType.APPLICATION_OCTET_STREAM); + } + + private String createConvertedFilename(String originalFilename, String suffix) { + if (originalFilename == null) { + return GeneralUtils.generateFilename(DEFAULT_COMIC_NAME, suffix); + } + + String baseName = EXTENSION_PATTERN.matcher(originalFilename).replaceFirst(""); + if (baseName.isBlank()) { + baseName = DEFAULT_COMIC_NAME; + } + + return GeneralUtils.generateFilename(baseName, suffix); } private String getMediaType(String imageFormat) { String mimeType = URLConnection.guessContentTypeFromName("." + imageFormat); - return "null".equals(mimeType) ? "application/octet-stream" : mimeType; + return "null".equals(mimeType) ? MediaType.APPLICATION_OCTET_STREAM_VALUE : mimeType; } /** diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertMarkdownToPdf.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertMarkdownToPdf.java index 3bacc7ce1..1c57161bb 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertMarkdownToPdf.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertMarkdownToPdf.java @@ -10,6 +10,7 @@ import org.commonmark.node.Node; import org.commonmark.parser.Parser; import org.commonmark.renderer.html.AttributeProvider; import org.commonmark.renderer.html.HtmlRenderer; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; @@ -25,11 +26,7 @@ import stirling.software.common.annotations.api.ConvertApi; import stirling.software.common.configuration.RuntimePathConfig; import stirling.software.common.model.api.GeneralFile; import stirling.software.common.service.CustomPDFDocumentFactory; -import stirling.software.common.util.CustomHtmlSanitizer; -import stirling.software.common.util.ExceptionUtils; -import stirling.software.common.util.FileToPdf; -import stirling.software.common.util.TempFileManager; -import stirling.software.common.util.WebResponseUtils; +import stirling.software.common.util.*; @ConvertApi @RequiredArgsConstructor @@ -42,7 +39,7 @@ public class ConvertMarkdownToPdf { private final CustomHtmlSanitizer customHtmlSanitizer; - @AutoJobPostMapping(consumes = "multipart/form-data", value = "/markdown/pdf") + @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/markdown/pdf") @StandardPdfResponse @Operation( summary = "Convert a Markdown file to PDF", @@ -86,9 +83,7 @@ public class ConvertMarkdownToPdf { tempFileManager, customHtmlSanitizer); pdfBytes = pdfDocumentFactory.createNewBytesBasedOnOldDocument(pdfBytes); - String outputFilename = - originalFilename.replaceFirst("[.][^.]+$", "") - + ".pdf"; // Remove file extension and append .pdf + String outputFilename = GeneralUtils.generateFilename(originalFilename, ".pdf"); return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename); } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertOfficeController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertOfficeController.java index 95f4b36cb..46ad437b3 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertOfficeController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertOfficeController.java @@ -5,12 +5,14 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; +import org.apache.commons.io.FileUtils; import org.apache.commons.io.FilenameUtils; import org.apache.pdfbox.pdmodel.PDDocument; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; @@ -19,81 +21,158 @@ import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import stirling.software.SPDF.config.EndpointConfiguration; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.ConvertApi; import stirling.software.common.configuration.RuntimePathConfig; import stirling.software.common.model.api.GeneralFile; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.CustomHtmlSanitizer; +import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.ProcessExecutor; import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult; +import stirling.software.common.util.RegexPatternUtils; import stirling.software.common.util.WebResponseUtils; @ConvertApi @RequiredArgsConstructor +@Slf4j public class ConvertOfficeController { private final CustomPDFDocumentFactory pdfDocumentFactory; private final RuntimePathConfig runtimePathConfig; private final CustomHtmlSanitizer customHtmlSanitizer; + private final EndpointConfiguration endpointConfiguration; + + private boolean isUnoconvertAvailable() { + return endpointConfiguration.isGroupEnabled("Unoconvert") + || endpointConfiguration.isGroupEnabled("Python"); + } public File convertToPdf(MultipartFile inputFile) throws IOException, InterruptedException { - // Check for valid file extension + // Check for valid file extension and sanitize filename String originalFilename = Filenames.toSimpleFileName(inputFile.getOriginalFilename()); - if (originalFilename == null - || !isValidFileExtension(FilenameUtils.getExtension(originalFilename))) { - throw new IllegalArgumentException("Invalid file extension"); + if (originalFilename == null || originalFilename.isBlank()) { + throw new IllegalArgumentException("Missing original filename"); } - // Save the uploaded file to a temporary location - Path tempInputFile = - Files.createTempFile("input_", "." + FilenameUtils.getExtension(originalFilename)); + // Check for valid file extension + String extension = FilenameUtils.getExtension(originalFilename); + if (extension == null || !isValidFileExtension(extension)) { + throw new IllegalArgumentException("Invalid file extension"); + } + String extensionLower = extension.toLowerCase(); + + String baseName = FilenameUtils.getBaseName(originalFilename); + if (baseName == null || baseName.isBlank()) { + baseName = "input"; + } + + // create temporary working directory + Path workDir = Files.createTempDirectory("office2pdf_"); + Path inputPath = workDir.resolve(baseName + "." + extensionLower); + Path outputPath = workDir.resolve(baseName + ".pdf"); // Check if the file is HTML and apply sanitization if needed - String fileExtension = FilenameUtils.getExtension(originalFilename).toLowerCase(); - if ("html".equals(fileExtension) || "htm".equals(fileExtension)) { + if ("html".equals(extensionLower) || "htm".equals(extensionLower)) { // Read and sanitize HTML content String htmlContent = new String(inputFile.getBytes(), StandardCharsets.UTF_8); String sanitizedHtml = customHtmlSanitizer.sanitize(htmlContent); - Files.write(tempInputFile, sanitizedHtml.getBytes(StandardCharsets.UTF_8)); + Files.writeString(inputPath, sanitizedHtml, StandardCharsets.UTF_8); } else { - inputFile.transferTo(tempInputFile); + // copy file content + Files.copy(inputFile.getInputStream(), inputPath, StandardCopyOption.REPLACE_EXISTING); } - // Prepare the output file path - Path tempOutputFile = Files.createTempFile("output_", ".pdf"); - try { - // Run the LibreOffice command - List command = - new ArrayList<>( - Arrays.asList( - runtimePathConfig.getUnoConvertPath(), - "--port", - "2003", - "--convert-to", - "pdf", - tempInputFile.toString(), - tempOutputFile.toString())); - ProcessExecutorResult returnCode = - ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE) - .runCommandWithOutputHandling(command); + ProcessExecutorResult result; + // Run Unoconvert command + if (isUnoconvertAvailable()) { + // Unoconvert: schreibe direkt in outputPath innerhalb des workDir + List command = new ArrayList<>(); + command.add(runtimePathConfig.getUnoConvertPath()); + command.add("--port"); + command.add("2003"); + command.add("--convert-to"); + command.add("pdf"); + command.add(inputPath.toString()); + command.add(outputPath.toString()); - // Read the converted PDF file - return tempOutputFile.toFile(); + result = + ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE) + .runCommandWithOutputHandling(command); + } // Run soffice command + else { + List command = new ArrayList<>(); + command.add("soffice"); + command.add("--headless"); + command.add("--nologo"); + command.add("--convert-to"); + command.add("pdf:writer_pdf_Export"); + command.add("--outdir"); + command.add(workDir.toString()); + command.add(inputPath.toString()); + + result = + ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE) + .runCommandWithOutputHandling(command); + } + + // Check the result + if (result == null) { + throw new IllegalStateException("Converter returned no result"); + } + if (result.getRc() != 0) { + throw new IllegalStateException("Conversion failed (exit " + result.getRc() + ")"); + } + + if (!Files.exists(outputPath)) { + // Some LibreOffice versions may deviate with exotic names – as a fallback, we try + // to find any .pdf in the workDir + try (var stream = Files.list(workDir)) { + Path fallback = + stream.filter( + p -> + p.getFileName() + .toString() + .toLowerCase() + .endsWith(".pdf")) + .findFirst() + .orElse(null); + if (fallback == null) { + throw new IllegalStateException("No PDF produced."); + } + // Move the found PDF to the expected outputPath + Files.move(fallback, outputPath, StandardCopyOption.REPLACE_EXISTING); + } + } + + // Check if the output file is empty + if (Files.size(outputPath) == 0L) { + throw new IllegalStateException("Produced PDF is empty"); + } + + return outputPath.toFile(); } finally { // Clean up the temporary files - if (tempInputFile != null) Files.deleteIfExists(tempInputFile); + try { + Files.deleteIfExists(inputPath); + } catch (IOException e) { + log.warn("Failed to delete temp input file: {}", inputPath, e); + } } } private boolean isValidFileExtension(String fileExtension) { - String extensionPattern = "^(?i)[a-z0-9]{2,4}$"; - return fileExtension.matches(extensionPattern); + return RegexPatternUtils.getInstance() + .getFileExtensionValidationPattern() + .matcher(fileExtension) + .matches(); } - @AutoJobPostMapping(consumes = "multipart/form-data", value = "/file/pdf") + @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/file/pdf") @Operation( summary = "Convert a file to a PDF using LibreOffice", description = @@ -111,11 +190,12 @@ public class ConvertOfficeController { PDDocument doc = pdfDocumentFactory.load(file); return WebResponseUtils.pdfDocToWebResponse( doc, - Filenames.toSimpleFileName(inputFile.getOriginalFilename()) - .replaceFirst("[.][^.]+$", "") - + "_convertedToPDF.pdf"); + GeneralUtils.generateFilename( + inputFile.getOriginalFilename(), "_convertedToPDF.pdf")); } finally { - if (file != null) file.delete(); + if (file != null && file.getParent() != null) { + FileUtils.deleteDirectory(file.getParentFile()); + } } } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToHtml.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToHtml.java index 6d19052ab..f6406ff25 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToHtml.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToHtml.java @@ -1,21 +1,28 @@ package stirling.software.SPDF.controller.api.converters; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; + import stirling.software.SPDF.config.swagger.HtmlConversionResponse; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.ConvertApi; import stirling.software.common.model.api.PDFFile; import stirling.software.common.util.PDFToFile; +import stirling.software.common.util.TempFileManager; @ConvertApi +@RequiredArgsConstructor public class ConvertPDFToHtml { - @AutoJobPostMapping(consumes = "multipart/form-data", value = "/pdf/html") + private final TempFileManager tempFileManager; + + @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/html") @Operation( summary = "Convert PDF to HTML", description = @@ -23,7 +30,7 @@ public class ConvertPDFToHtml { @HtmlConversionResponse public ResponseEntity processPdfToHTML(@ModelAttribute PDFFile file) throws Exception { MultipartFile inputFile = file.getFileInput(); - PDFToFile pdfToFile = new PDFToFile(); + PDFToFile pdfToFile = new PDFToFile(tempFileManager); return pdfToFile.processPdfToHtml(inputFile); } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToOffice.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToOffice.java index afab1ca62..23317f94c 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToOffice.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToOffice.java @@ -9,7 +9,6 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; -import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; @@ -25,7 +24,9 @@ import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.ConvertApi; import stirling.software.common.model.api.PDFFile; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.PDFToFile; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @ConvertApi @@ -33,8 +34,9 @@ import stirling.software.common.util.WebResponseUtils; public class ConvertPDFToOffice { private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; - @AutoJobPostMapping(consumes = "multipart/form-data", value = "/pdf/presentation") + @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/presentation") @PowerPointConversionResponse @Operation( summary = "Convert PDF to Presentation format", @@ -46,11 +48,11 @@ public class ConvertPDFToOffice { throws IOException, InterruptedException { MultipartFile inputFile = request.getFileInput(); String outputFormat = request.getOutputFormat(); - PDFToFile pdfToFile = new PDFToFile(); + PDFToFile pdfToFile = new PDFToFile(tempFileManager); return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "impress_pdf_import"); } - @AutoJobPostMapping(consumes = "multipart/form-data", value = "/pdf/text") + @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/text") @TextPlainConversionResponse @Operation( summary = "Convert PDF to Text or RTF format", @@ -68,18 +70,16 @@ public class ConvertPDFToOffice { String text = stripper.getText(document); return WebResponseUtils.bytesToWebResponse( text.getBytes(), - Filenames.toSimpleFileName(inputFile.getOriginalFilename()) - .replaceFirst("[.][^.]+$", "") - + ".txt", + GeneralUtils.generateFilename(inputFile.getOriginalFilename(), ".txt"), MediaType.TEXT_PLAIN); } } else { - PDFToFile pdfToFile = new PDFToFile(); + PDFToFile pdfToFile = new PDFToFile(tempFileManager); return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "writer_pdf_import"); } } - @AutoJobPostMapping(consumes = "multipart/form-data", value = "/pdf/word") + @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/word") @WordConversionResponse @Operation( summary = "Convert PDF to Word document", @@ -90,11 +90,11 @@ public class ConvertPDFToOffice { throws IOException, InterruptedException { MultipartFile inputFile = request.getFileInput(); String outputFormat = request.getOutputFormat(); - PDFToFile pdfToFile = new PDFToFile(); + PDFToFile pdfToFile = new PDFToFile(tempFileManager); return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "writer_pdf_import"); } - @AutoJobPostMapping(consumes = "multipart/form-data", value = "/pdf/xml") + @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/xml") @XmlConversionResponse @Operation( summary = "Convert PDF to XML", @@ -104,7 +104,7 @@ public class ConvertPDFToOffice { public ResponseEntity processPdfToXML(@ModelAttribute PDFFile file) throws Exception { MultipartFile inputFile = file.getFileInput(); - PDFToFile pdfToFile = new PDFToFile(); + PDFToFile pdfToFile = new PDFToFile(tempFileManager); return pdfToFile.processPdfToOfficeFormat(inputFile, "xml", "writer_pdf_import"); } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToPDFA.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToPDFA.java index a3fc0e5c7..d8acb35ae 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToPDFA.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToPDFA.java @@ -7,16 +7,18 @@ import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Arrays; -import java.util.Calendar; import java.util.Collections; +import java.util.GregorianCalendar; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.TimeZone; import org.apache.commons.io.FileUtils; import org.apache.pdfbox.Loader; @@ -75,7 +77,7 @@ import stirling.software.common.util.WebResponseUtils; @Slf4j public class ConvertPDFToPDFA { - @AutoJobPostMapping(consumes = "multipart/form-data", value = "/pdf/pdfa") + @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/pdfa") @StandardPdfResponse @Operation( summary = "Convert a PDF to a PDF/A", @@ -87,7 +89,7 @@ public class ConvertPDFToPDFA { String outputFormat = request.getOutputFormat(); // Validate input file type - if (!"application/pdf".equals(inputFile.getContentType())) { + if (!MediaType.APPLICATION_PDF_VALUE.equals(inputFile.getContentType())) { log.error("Invalid input file type: {}", inputFile.getContentType()); throw ExceptionUtils.createPdfFileRequiredException(); } @@ -121,7 +123,7 @@ public class ConvertPDFToPDFA { preProcessedFile = preProcessHighlights(tempInputFile.toFile()); } Set missingFonts = new HashSet<>(); - boolean needImgs = false; + boolean needImgs; try (PDDocument doc = Loader.loadPDF(preProcessedFile)) { missingFonts = findUnembeddedFontNames(doc); needImgs = (pdfaPart == 1) && hasTransparentImages(doc); @@ -283,7 +285,7 @@ public class ConvertPDFToPDFA { if (fontStream == null) continue; try (InputStream in = fontStream.createInputStream()) { - PDFont newFont = null; + PDFont newFont; try { newFont = PDType0Font.load(baseDoc, in, false); } catch (IOException e1) { @@ -562,18 +564,31 @@ public class ConvertPDFToPDFA { adobePdfSchema.setKeywords(keywords); } - // Set creation and modification dates - Calendar now = Calendar.getInstance(TimeZone.getTimeZone("UTC")); - Calendar originalCreationDate = docInfo.getCreationDate(); - if (originalCreationDate == null) { - originalCreationDate = now; - } - docInfo.setCreationDate(originalCreationDate); - xmpBasicSchema.setCreateDate(originalCreationDate); + // Set creation and modification dates using java.time and convert to GregorianCalendar + Instant nowInstant = Instant.now(); + ZonedDateTime nowZdt = ZonedDateTime.ofInstant(nowInstant, ZoneId.of("UTC")); + GregorianCalendar nowCal = GregorianCalendar.from(nowZdt); - docInfo.setModificationDate(now); - xmpBasicSchema.setModifyDate(now); - xmpBasicSchema.setMetadataDate(now); + java.util.Calendar originalCreationDate = docInfo.getCreationDate(); + GregorianCalendar creationCal; + if (originalCreationDate == null) { + creationCal = nowCal; + } else if (originalCreationDate instanceof GregorianCalendar) { + creationCal = (GregorianCalendar) originalCreationDate; + } else { + // convert other Calendar implementations to GregorianCalendar preserving instant + creationCal = + GregorianCalendar.from( + ZonedDateTime.ofInstant( + originalCreationDate.toInstant(), ZoneId.of("UTC"))); + } + + docInfo.setCreationDate(creationCal); + xmpBasicSchema.setCreateDate(creationCal); + + docInfo.setModificationDate(nowCal); + xmpBasicSchema.setModifyDate(nowCal); + xmpBasicSchema.setMetadataDate(nowCal); // Serialize the created metadata so it can be attached to the existent metadata ByteArrayOutputStream xmpOut = new ByteArrayOutputStream(); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPDF.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPDF.java index f8c190856..76aa4afef 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPDF.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPDF.java @@ -1,14 +1,19 @@ package stirling.software.SPDF.controller.api.converters; import java.io.IOException; +import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import org.apache.pdfbox.pdmodel.PDDocument; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; +import org.springframework.web.util.UriComponentsBuilder; import io.swagger.v3.oas.annotations.Operation; @@ -22,10 +27,9 @@ import stirling.software.common.annotations.api.ConvertApi; import stirling.software.common.configuration.RuntimePathConfig; import stirling.software.common.model.ApplicationProperties; import stirling.software.common.service.CustomPDFDocumentFactory; -import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.ProcessExecutor; -import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult; +import stirling.software.common.util.RegexPatternUtils; import stirling.software.common.util.WebResponseUtils; @ConvertApi @@ -37,31 +41,51 @@ public class ConvertWebsiteToPDF { private final RuntimePathConfig runtimePathConfig; private final ApplicationProperties applicationProperties; - @AutoJobPostMapping(consumes = "multipart/form-data", value = "/url/pdf") + @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/url/pdf") @StandardPdfResponse @Operation( summary = "Convert a URL to a PDF", description = "This endpoint fetches content from a URL and converts it to a PDF format." + " Input:N/A Output:PDF Type:SISO") - public ResponseEntity urlToPdf(@ModelAttribute UrlToPdfRequest request) + public ResponseEntity urlToPdf(@ModelAttribute UrlToPdfRequest request) throws IOException, InterruptedException { String URL = request.getUrlInput(); + UriComponentsBuilder uriComponentsBuilder = + ServletUriComponentsBuilder.fromCurrentContextPath().path("/url-to-pdf"); + URI location = null; + HttpStatus status = HttpStatus.SEE_OTHER; if (!applicationProperties.getSystem().getEnableUrlToPDF()) { - throw ExceptionUtils.createIllegalArgumentException( - "error.endpointDisabled", "This endpoint has been disabled by the admin"); - } - // Validate the URL format - if (!URL.matches("^https?://.*") || !GeneralUtils.isValidURL(URL)) { - throw ExceptionUtils.createInvalidArgumentException( - "URL", "provided format is invalid"); + location = + uriComponentsBuilder + .queryParam("error", "error.endpointDisabled") + .build() + .toUri(); + } else { + // Validate the URL format (relaxed: only invalid if BOTH checks fail) + boolean patternValid = + RegexPatternUtils.getInstance().getHttpUrlPattern().matcher(URL).matches(); + boolean generalValid = GeneralUtils.isValidURL(URL); + if (!patternValid && !generalValid) { + location = + uriComponentsBuilder + .queryParam("error", "error.invalidUrlFormat") + .build() + .toUri(); + } else if (!GeneralUtils.isURLReachable(URL)) { + // validate the URL is reachable + location = + uriComponentsBuilder + .queryParam("error", "error.urlNotReachable") + .build() + .toUri(); + } } - // validate the URL is reachable - if (!GeneralUtils.isURLReachable(URL)) { - throw ExceptionUtils.createIllegalArgumentException( - "error.urlNotReachable", "URL is not reachable, please provide a valid URL"); + if (location != null) { + log.info("Redirecting to: {}", location.toString()); + return ResponseEntity.status(status).location(location).build(); } Path tempOutputFile = null; @@ -77,9 +101,8 @@ public class ConvertWebsiteToPDF { command.add("--pdf-forms"); command.add(tempOutputFile.toString()); - ProcessExecutorResult returnCode = - ProcessExecutor.getInstance(ProcessExecutor.Processes.WEASYPRINT) - .runCommandWithOutputHandling(command); + ProcessExecutor.getInstance(ProcessExecutor.Processes.WEASYPRINT) + .runCommandWithOutputHandling(command); // Load the PDF using pdfDocumentFactory doc = pdfDocumentFactory.load(tempOutputFile.toFile()); @@ -87,7 +110,13 @@ public class ConvertWebsiteToPDF { // Convert URL to a safe filename String outputFilename = convertURLToFileName(URL); - return WebResponseUtils.pdfDocToWebResponse(doc, outputFilename); + ResponseEntity response = + WebResponseUtils.pdfDocToWebResponse(doc, outputFilename); + if (response == null) { + // Defensive fallback - should not happen but avoids null returns breaking tests + return ResponseEntity.ok(new byte[0]); + } + return response; } finally { if (tempOutputFile != null) { @@ -101,10 +130,39 @@ public class ConvertWebsiteToPDF { } private String convertURLToFileName(String url) { - String safeName = url.replaceAll("[^a-zA-Z0-9]", "_"); + String safeName = GeneralUtils.convertToFileName(url); + if (safeName == null || safeName.isBlank()) { + // Fallback: derive from URL host/path or use default + try { + URI uri = URI.create(url); + String hostPart = uri.getHost(); + if (hostPart == null || hostPart.isBlank()) { + hostPart = "document"; + } + safeName = + RegexPatternUtils.getInstance() + .getNonAlnumUnderscorePattern() + .matcher(hostPart) + .replaceAll("_"); + } catch (Exception e) { + safeName = "document"; + } + } + // Restrict characters strictly to alphanumeric and underscore for predictable tests + RegexPatternUtils patterns = RegexPatternUtils.getInstance(); + safeName = patterns.getNonAlnumUnderscorePattern().matcher(safeName).replaceAll("_"); + // Collapse multiple underscores + safeName = patterns.getMultipleUnderscoresPattern().matcher(safeName).replaceAll("_"); + // Trim leading underscores + safeName = patterns.getLeadingUnderscoresPattern().matcher(safeName).replaceAll(""); + // Trim trailing underscores + safeName = patterns.getTrailingUnderscoresPattern().matcher(safeName).replaceAll(""); + if (safeName.isEmpty()) { + safeName = "document"; + } if (safeName.length() > 50) { safeName = safeName.substring(0, 50); // restrict to 50 characters } - return safeName + ".pdf"; + return GeneralUtils.generateFilename(safeName, ".pdf"); } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ExtractCSVController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ExtractCSVController.java index e787b22ee..84ca61ffa 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ExtractCSVController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ExtractCSVController.java @@ -30,6 +30,7 @@ import stirling.software.SPDF.pdf.FlexibleCSVWriter; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.ConvertApi; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.GeneralUtils; import technology.tabula.ObjectExtractor; import technology.tabula.Page; @@ -43,7 +44,7 @@ public class ExtractCSVController { private final CustomPDFDocumentFactory pdfDocumentFactory; - @AutoJobPostMapping(value = "/pdf/csv", consumes = "multipart/form-data") + @AutoJobPostMapping(value = "/pdf/csv", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @CsvConversionResponse @Operation( summary = "Extracts a CSV document from a PDF", @@ -125,7 +126,7 @@ public class ExtractCSVController { } private String getBaseName(String filename) { - return filename.replaceFirst("[.][^.]+$", ""); + return GeneralUtils.removeExtension(filename); } private record CsvEntry(String filename, String content) {} diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/filters/FilterController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/filters/FilterController.java index 0b130ac48..551d3121d 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/filters/FilterController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/filters/FilterController.java @@ -5,6 +5,7 @@ import java.io.IOException; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; @@ -34,7 +35,9 @@ public class FilterController { private final CustomPDFDocumentFactory pdfDocumentFactory; - @AutoJobPostMapping(consumes = "multipart/form-data", value = "/filter-contains-text") + @AutoJobPostMapping( + consumes = MediaType.MULTIPART_FORM_DATA_VALUE, + value = "/filter-contains-text") @FilterResponse @Operation( summary = "Checks if a PDF contains set text, returns true if does", @@ -45,15 +48,19 @@ public class FilterController { String text = request.getText(); String pageNumber = request.getPageNumbers(); - PDDocument pdfDocument = pdfDocumentFactory.load(inputFile); - if (PdfUtils.hasText(pdfDocument, pageNumber, text)) - return WebResponseUtils.pdfDocToWebResponse( - pdfDocument, Filenames.toSimpleFileName(inputFile.getOriginalFilename())); + try (PDDocument pdfDocument = pdfDocumentFactory.load(inputFile)) { + if (PdfUtils.hasText(pdfDocument, pageNumber, text)) { + return WebResponseUtils.pdfDocToWebResponse( + pdfDocument, Filenames.toSimpleFileName(inputFile.getOriginalFilename())); + } + } return null; } // TODO - @AutoJobPostMapping(consumes = "multipart/form-data", value = "/filter-contains-image") + @AutoJobPostMapping( + consumes = MediaType.MULTIPART_FORM_DATA_VALUE, + value = "/filter-contains-image") @FilterResponse @Operation( summary = "Checks if a PDF contains an image", @@ -70,7 +77,9 @@ public class FilterController { return null; } - @AutoJobPostMapping(consumes = "multipart/form-data", value = "/filter-page-count") + @AutoJobPostMapping( + consumes = MediaType.MULTIPART_FORM_DATA_VALUE, + value = "/filter-page-count") @FilterResponse @Operation( summary = "Checks if a PDF is greater, less or equal to a setPageCount", @@ -83,28 +92,22 @@ public class FilterController { // Load the PDF PDDocument document = pdfDocumentFactory.load(inputFile); int actualPageCount = document.getNumberOfPages(); - - boolean valid = false; // Perform the comparison - switch (comparator) { - case "Greater": - valid = actualPageCount > pageCount; - break; - case "Equal": - valid = actualPageCount == pageCount; - break; - case "Less": - valid = actualPageCount < pageCount; - break; - default: - throw ExceptionUtils.createInvalidArgumentException("comparator", comparator); - } + boolean valid = + switch (comparator) { + case "Greater" -> actualPageCount > pageCount; + case "Equal" -> actualPageCount == pageCount; + case "Less" -> actualPageCount < pageCount; + default -> + throw ExceptionUtils.createInvalidArgumentException( + "comparator", comparator); + }; if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile); return null; } - @AutoJobPostMapping(consumes = "multipart/form-data", value = "/filter-page-size") + @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-page-size") @FilterResponse @Operation( summary = "Checks if a PDF is of a certain size", @@ -128,27 +131,22 @@ public class FilterController { PDRectangle standardSize = PdfUtils.textToPageSize(standardPageSize); float standardArea = standardSize.getWidth() * standardSize.getHeight(); - boolean valid = false; // Perform the comparison - switch (comparator) { - case "Greater": - valid = actualArea > standardArea; - break; - case "Equal": - valid = actualArea == standardArea; - break; - case "Less": - valid = actualArea < standardArea; - break; - default: - throw ExceptionUtils.createInvalidArgumentException("comparator", comparator); - } + boolean valid = + switch (comparator) { + case "Greater" -> actualArea > standardArea; + case "Equal" -> actualArea == standardArea; + case "Less" -> actualArea < standardArea; + default -> + throw ExceptionUtils.createInvalidArgumentException( + "comparator", comparator); + }; if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile); return null; } - @AutoJobPostMapping(consumes = "multipart/form-data", value = "/filter-file-size") + @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-file-size") @FilterResponse @Operation( summary = "Checks if a PDF is a set file size", @@ -162,27 +160,24 @@ public class FilterController { // Get the file size long actualFileSize = inputFile.getSize(); - boolean valid = false; // Perform the comparison - switch (comparator) { - case "Greater": - valid = actualFileSize > fileSize; - break; - case "Equal": - valid = actualFileSize == fileSize; - break; - case "Less": - valid = actualFileSize < fileSize; - break; - default: - throw ExceptionUtils.createInvalidArgumentException("comparator", comparator); - } + boolean valid = + switch (comparator) { + case "Greater" -> actualFileSize > fileSize; + case "Equal" -> actualFileSize == fileSize; + case "Less" -> actualFileSize < fileSize; + default -> + throw ExceptionUtils.createInvalidArgumentException( + "comparator", comparator); + }; if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile); return null; } - @AutoJobPostMapping(consumes = "multipart/form-data", value = "/filter-page-rotation") + @AutoJobPostMapping( + consumes = MediaType.MULTIPART_FORM_DATA_VALUE, + value = "/filter-page-rotation") @FilterResponse @Operation( summary = "Checks if a PDF is of a certain rotation", @@ -199,21 +194,17 @@ public class FilterController { // Get the rotation of the first page PDPage firstPage = document.getPage(0); int actualRotation = firstPage.getRotation(); - boolean valid = false; + // Perform the comparison - switch (comparator) { - case "Greater": - valid = actualRotation > rotation; - break; - case "Equal": - valid = actualRotation == rotation; - break; - case "Less": - valid = actualRotation < rotation; - break; - default: - throw ExceptionUtils.createInvalidArgumentException("comparator", comparator); - } + boolean valid = + switch (comparator) { + case "Greater" -> actualRotation > rotation; + case "Equal" -> actualRotation == rotation; + case "Less" -> actualRotation < rotation; + default -> + throw ExceptionUtils.createInvalidArgumentException( + "comparator", comparator); + }; if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile); return null; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AttachmentController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AttachmentController.java index b02dbb240..0647f491a 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AttachmentController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AttachmentController.java @@ -4,6 +4,7 @@ import java.io.IOException; import java.util.List; import org.apache.pdfbox.pdmodel.PDDocument; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; @@ -20,6 +21,7 @@ import stirling.software.SPDF.service.AttachmentServiceInterface; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.MiscApi; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.WebResponseUtils; @MiscApi @@ -31,7 +33,7 @@ public class AttachmentController { private final AttachmentServiceInterface pdfAttachmentService; - @AutoJobPostMapping(consumes = "multipart/form-data", value = "/add-attachments") + @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/add-attachments") @StandardPdfResponse @Operation( summary = "Add attachments to PDF", @@ -48,8 +50,8 @@ public class AttachmentController { return WebResponseUtils.pdfDocToWebResponse( document, - Filenames.toSimpleFileName(fileInput.getOriginalFilename()) - .replaceFirst("[.][^.]+$", "") - + "_with_attachments.pdf"); + GeneralUtils.generateFilename( + Filenames.toSimpleFileName(fileInput.getOriginalFilename()), + "_with_attachments.pdf")); } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AutoRenameController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AutoRenameController.java index 1545945f9..3d8a122ba 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AutoRenameController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AutoRenameController.java @@ -8,6 +8,7 @@ import java.util.List; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.text.PDFTextStripper; import org.apache.pdfbox.text.TextPosition; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; @@ -22,6 +23,7 @@ import stirling.software.SPDF.model.api.misc.ExtractHeaderRequest; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.MiscApi; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.RegexPatternUtils; import stirling.software.common.util.WebResponseUtils; @MiscApi @@ -34,7 +36,7 @@ public class AutoRenameController { private final CustomPDFDocumentFactory pdfDocumentFactory; - @AutoJobPostMapping(consumes = "multipart/form-data", value = "/auto-rename") + @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/auto-rename") @Operation( summary = "Extract header from PDF file", description = @@ -71,7 +73,7 @@ public class AutoRenameController { } private void processLine() { - if (lineBuilder.length() > 0 && lineCount < LINE_LIMIT) { + if (!lineBuilder.isEmpty() && lineCount < LINE_LIMIT) { lineInfos.add(new LineInfo(lineBuilder.toString(), maxFontSizeInLine)); } } @@ -89,14 +91,14 @@ public class AutoRenameController { // Merge lines with same font size List mergedLineInfos = new ArrayList<>(); for (int i = 0; i < lineInfos.size(); i++) { - String mergedText = lineInfos.get(i).text; + StringBuilder mergedText = new StringBuilder(lineInfos.get(i).text); float fontSize = lineInfos.get(i).fontSize; while (i + 1 < lineInfos.size() && lineInfos.get(i + 1).fontSize == fontSize) { - mergedText += " " + lineInfos.get(i + 1).text; + mergedText.append(" ").append(lineInfos.get(i + 1).text); i++; } - mergedLineInfos.add(new LineInfo(mergedText, fontSize)); + mergedLineInfos.add(new LineInfo(mergedText.toString(), fontSize)); } // Sort lines by font size in descending order and get the first one @@ -130,7 +132,12 @@ public class AutoRenameController { // Sanitize the header string by removing characters not allowed in a filename. if (header != null && header.length() < 255) { - header = header.replaceAll("[/\\\\?%*:|\"<>]", "").trim(); + header = + RegexPatternUtils.getInstance() + .getSafeFilenamePattern() + .matcher(header) + .replaceAll("") + .trim(); return WebResponseUtils.pdfDocToWebResponse(document, header + ".pdf"); } else { log.info("File has no good title to be found"); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AutoSplitPdfController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AutoSplitPdfController.java index ee8e3847e..0c1b1cc2e 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AutoSplitPdfController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AutoSplitPdfController.java @@ -6,7 +6,6 @@ import java.awt.image.DataBufferInt; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.file.Files; -import java.nio.file.Path; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -34,7 +33,13 @@ import stirling.software.SPDF.config.swagger.MultiFileResponse; import stirling.software.SPDF.model.api.misc.AutoSplitPdfRequest; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.MiscApi; +import stirling.software.common.model.ApplicationProperties; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.ApplicationContextProvider; +import stirling.software.common.util.ExceptionUtils; +import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @MiscApi @@ -50,6 +55,7 @@ public class AutoSplitPdfController { "https://stirlingpdf.com")); private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; private static String decodeQRCode(BufferedImage bufferedImage) { LuminanceSource source; @@ -99,7 +105,7 @@ public class AutoSplitPdfController { } } - @AutoJobPostMapping(value = "/auto-split-pdf", consumes = "multipart/form-data") + @AutoJobPostMapping(value = "/auto-split-pdf", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @MultiFileResponse @Operation( summary = "Auto split PDF pages into separate documents", @@ -115,16 +121,32 @@ public class AutoSplitPdfController { PDDocument document = null; List splitDocuments = new ArrayList<>(); - Path zipFile = null; - byte[] data = null; + TempFile outputTempFile = null; try { + outputTempFile = new TempFile(tempFileManager, ".zip"); document = pdfDocumentFactory.load(file.getInputStream()); PDFRenderer pdfRenderer = new PDFRenderer(document); pdfRenderer.setSubsamplingAllowed(true); for (int page = 0; page < document.getNumberOfPages(); ++page) { - BufferedImage bim = pdfRenderer.renderImageWithDPI(page, 150); + BufferedImage bim; + + // Use global maximum DPI setting, fallback to 300 if not set + int renderDpi = 150; // Default fallback + ApplicationProperties properties = + ApplicationContextProvider.getBean(ApplicationProperties.class); + if (properties != null && properties.getSystem() != null) { + renderDpi = properties.getSystem().getMaxDPI(); + } + + try { + bim = pdfRenderer.renderImageWithDPI(page, renderDpi); + } catch (OutOfMemoryError e) { + throw ExceptionUtils.createOutOfMemoryDpiException(page + 1, renderDpi, e); + } catch (NegativeArraySizeException e) { + throw ExceptionUtils.createOutOfMemoryDpiException(page + 1, renderDpi, e); + } String result = decodeQRCode(bim); boolean isValidQrCode = VALID_QR_CONTENTS.contains(result); @@ -150,12 +172,12 @@ public class AutoSplitPdfController { // Remove split documents that have no pages splitDocuments.removeIf(pdDocument -> pdDocument.getNumberOfPages() == 0); - zipFile = Files.createTempFile("split_documents", ".zip"); String filename = - Filenames.toSimpleFileName(file.getOriginalFilename()) - .replaceFirst("[.][^.]+$", ""); + GeneralUtils.removeExtension( + Filenames.toSimpleFileName(file.getOriginalFilename())); - try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile))) { + try (ZipOutputStream zipOut = + new ZipOutputStream(Files.newOutputStream(outputTempFile.getPath()))) { for (int i = 0; i < splitDocuments.size(); i++) { String fileName = filename + "_" + (i + 1) + ".pdf"; PDDocument splitDocument = splitDocuments.get(i); @@ -171,10 +193,10 @@ public class AutoSplitPdfController { } } - data = Files.readAllBytes(zipFile); - + byte[] data = Files.readAllBytes(outputTempFile.getPath()); return WebResponseUtils.bytesToWebResponse( data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM); + } catch (Exception e) { log.error("Error in auto split", e); throw e; @@ -196,12 +218,8 @@ public class AutoSplitPdfController { } } - if (zipFile != null) { - try { - Files.deleteIfExists(zipFile); - } catch (IOException e) { - log.error("Error deleting temporary zip file", e); - } + if (outputTempFile != null) { + outputTempFile.close(); } } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/BlankPageController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/BlankPageController.java index 51f4aee68..30a15797e 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/BlankPageController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/BlankPageController.java @@ -28,7 +28,11 @@ import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.model.api.misc.RemoveBlankPagesRequest; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.MiscApi; +import stirling.software.common.model.ApplicationProperties; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.ApplicationContextProvider; +import stirling.software.common.util.ExceptionUtils; +import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.PdfUtils; import stirling.software.common.util.WebResponseUtils; @@ -65,7 +69,7 @@ public class BlankPageController { return whitePixelPercentage >= whitePercent; } - @AutoJobPostMapping(consumes = "multipart/form-data", value = "/remove-blanks") + @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/remove-blanks") @Operation( summary = "Remove blank pages from a PDF file", description = @@ -104,7 +108,25 @@ public class BlankPageController { if (hasImages) { log.info("page {} has image, running blank detection", pageIndex); // Render image and save as temp file - BufferedImage image = pdfRenderer.renderImageWithDPI(pageIndex, 30); + BufferedImage image; + + // Use global maximum DPI setting + int renderDpi = 30; // Default fallback + ApplicationProperties properties = + ApplicationContextProvider.getBean(ApplicationProperties.class); + if (properties != null && properties.getSystem() != null) { + renderDpi = properties.getSystem().getMaxDPI(); + } + + try { + image = pdfRenderer.renderImageWithDPI(pageIndex, renderDpi); + } catch (OutOfMemoryError e) { + throw ExceptionUtils.createOutOfMemoryDpiException( + pageIndex + 1, renderDpi, e); + } catch (NegativeArraySizeException e) { + throw ExceptionUtils.createOutOfMemoryDpiException( + pageIndex + 1, renderDpi, e); + } blank = isBlankImage(image, threshold, whitePercent, threshold); } } @@ -124,8 +146,8 @@ public class BlankPageController { ZipOutputStream zos = new ZipOutputStream(baos); String filename = - Filenames.toSimpleFileName(inputFile.getOriginalFilename()) - .replaceFirst("[.][^.]+$", ""); + GeneralUtils.removeExtension( + Filenames.toSimpleFileName(inputFile.getOriginalFilename())); if (!nonBlankPages.isEmpty()) { createZipEntry(zos, nonBlankPages, filename + "_nonBlankPages.pdf"); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/CompressController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/CompressController.java index 37ee16271..865a95c5c 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/CompressController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/CompressController.java @@ -10,12 +10,8 @@ import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Iterator; +import java.util.*; import java.util.List; -import java.util.Map; import java.util.Map.Entry; import javax.imageio.IIOImage; @@ -32,18 +28,14 @@ import org.apache.pdfbox.pdmodel.PDResources; import org.apache.pdfbox.pdmodel.graphics.PDXObject; import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; -import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.RequiredArgsConstructor; +import lombok.*; import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.config.EndpointConfiguration; @@ -655,7 +647,7 @@ public class CompressController { }; } - @AutoJobPostMapping(consumes = "multipart/form-data", value = "/compress-pdf") + @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/compress-pdf") @StandardPdfResponse @Operation( summary = "Optimize PDF file", @@ -780,11 +772,8 @@ public class CompressController { // Check if we can't increase the level further if (newOptimizeLevel == optimizeLevel) { - if (autoMode) { - log.info( - "Maximum optimization level reached without meeting target size."); - sizeMet = true; - } + log.info("Maximum optimization level reached without meeting target size."); + sizeMet = true; } else { // Reset flags for next iteration with higher optimization level imageCompressionApplied = false; @@ -803,9 +792,8 @@ public class CompressController { } String outputFilename = - Filenames.toSimpleFileName(inputFile.getOriginalFilename()) - .replaceFirst("[.][^.]+$", "") - + "_Optimized.pdf"; + GeneralUtils.generateFilename( + inputFile.getOriginalFilename(), "_Optimized.pdf"); return WebResponseUtils.pdfDocToWebResponse( pdfDocumentFactory.load(currentFile.toFile()), outputFilename); @@ -816,7 +804,7 @@ public class CompressController { try { Files.deleteIfExists(tempFile); } catch (IOException e) { - log.warn("Failed to delete temporary file: " + tempFile, e); + log.warn("Failed to delete temporary file: {}", tempFile, e); } } } @@ -886,7 +874,7 @@ public class CompressController { command.add("-sOutputFile=" + gsOutputFile.toString()); command.add(currentFile.toString()); - ProcessExecutorResult returnCode = null; + ProcessExecutorResult returnCode; try { returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT) diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/DecompressPdfController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/DecompressPdfController.java index 675d42acd..2abfaa8af 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/DecompressPdfController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/DecompressPdfController.java @@ -25,6 +25,7 @@ import stirling.software.common.annotations.api.MiscApi; import stirling.software.common.model.api.PDFFile; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; +import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.WebResponseUtils; @MiscApi @@ -34,7 +35,7 @@ public class DecompressPdfController { private final CustomPDFDocumentFactory pdfDocumentFactory; - @AutoJobPostMapping(value = "/decompress-pdf", consumes = "multipart/form-data") + @AutoJobPostMapping(value = "/decompress-pdf", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @Operation( summary = "Decompress PDF streams", description = "Fully decompresses all PDF streams including text content") @@ -51,10 +52,10 @@ public class DecompressPdfController { ByteArrayOutputStream baos = new ByteArrayOutputStream(); document.save(baos, CompressParameters.NO_COMPRESSION); - String outputFilename = - file.getOriginalFilename().replaceFirst("\\.(?=[^.]+$)", "_decompressed."); + // Return the PDF as a response return WebResponseUtils.bytesToWebResponse( - baos.toByteArray(), outputFilename, MediaType.APPLICATION_PDF); + baos.toByteArray(), + GeneralUtils.generateFilename(file.getOriginalFilename(), "_decompressed.pdf")); } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImageScansController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImageScansController.java index 1fa04d5d6..15fc9cd56 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImageScansController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImageScansController.java @@ -8,6 +8,7 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.stream.Stream; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @@ -30,7 +31,9 @@ import stirling.software.SPDF.config.swagger.MultiFileResponse; import stirling.software.SPDF.model.api.misc.ExtractImageScansRequest; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.MiscApi; +import stirling.software.common.model.ApplicationProperties; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.ApplicationContextProvider; import stirling.software.common.util.CheckProgramInstall; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; @@ -47,7 +50,9 @@ public class ExtractImageScansController { private final CustomPDFDocumentFactory pdfDocumentFactory; - @AutoJobPostMapping(consumes = "multipart/form-data", value = "/extract-image-scans") + @AutoJobPostMapping( + consumes = MediaType.MULTIPART_FORM_DATA_VALUE, + value = "/extract-image-scans") @MultiFileResponse @Operation( summary = "Extract image scans from an input file", @@ -67,7 +72,7 @@ public class ExtractImageScansController { List images = new ArrayList<>(); List tempImageFiles = new ArrayList<>(); - Path tempInputFile = null; + Path tempInputFile; Path tempZipFile = null; List tempDirs = new ArrayList<>(); @@ -94,7 +99,23 @@ public class ExtractImageScansController { Path tempFile = Files.createTempFile("image_", ".png"); // Render image and save as temp file - BufferedImage image = pdfRenderer.renderImageWithDPI(i, 300); + BufferedImage image; + + // Use global maximum DPI setting, fallback to 300 if not set + int renderDpi = 300; // Default fallback + ApplicationProperties properties = + ApplicationContextProvider.getBean(ApplicationProperties.class); + if (properties != null && properties.getSystem() != null) { + renderDpi = properties.getSystem().getMaxDPI(); + } + + try { + image = pdfRenderer.renderImageWithDPI(i, renderDpi); + } catch (OutOfMemoryError e) { + throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, renderDpi, e); + } catch (NegativeArraySizeException e) { + throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, renderDpi, e); + } ImageIO.write(image, "png", tempFile.toFile()); // Add temp file path to images list @@ -140,7 +161,10 @@ public class ExtractImageScansController { .runCommandWithOutputHandling(command); // Read the output photos in temp directory - List tempOutputFiles = Files.list(tempDir).sorted().toList(); + List tempOutputFiles; + try (Stream listStream = Files.list(tempDir)) { + tempOutputFiles = listStream.sorted().toList(); + } for (Path tempOutputFile : tempOutputFiles) { byte[] imageBytes = Files.readAllBytes(tempOutputFile); processedImageBytes.add(imageBytes); @@ -152,7 +176,7 @@ public class ExtractImageScansController { // Create zip file if multiple images if (processedImageBytes.size() > 1) { String outputZipFilename = - fileName.replaceFirst(REPLACEFIRST, "") + "_processed.zip"; + GeneralUtils.generateFilename(fileName, "_processed.zip"); tempZipFile = Files.createTempFile("output_", ".zip"); try (ZipOutputStream zipOut = @@ -161,10 +185,8 @@ public class ExtractImageScansController { for (int i = 0; i < processedImageBytes.size(); i++) { ZipEntry entry = new ZipEntry( - fileName.replaceFirst(REPLACEFIRST, "") - + "_" - + (i + 1) - + ".png"); + GeneralUtils.generateFilename( + fileName, "_processed_" + (i + 1) + ".png")); zipOut.putNextEntry(entry); zipOut.write(processedImageBytes.get(i)); zipOut.closeEntry(); @@ -179,7 +201,7 @@ public class ExtractImageScansController { return WebResponseUtils.bytesToWebResponse( zipBytes, outputZipFilename, MediaType.APPLICATION_OCTET_STREAM); } - if (processedImageBytes.size() == 0) { + if (processedImageBytes.isEmpty()) { throw new IllegalArgumentException("No images detected"); } else { @@ -187,7 +209,7 @@ public class ExtractImageScansController { byte[] imageBytes = processedImageBytes.get(0); return WebResponseUtils.bytesToWebResponse( imageBytes, - fileName.replaceFirst(REPLACEFIRST, "") + ".png", + GeneralUtils.generateFilename(fileName, ".png"), MediaType.IMAGE_PNG); } } finally { @@ -197,7 +219,7 @@ public class ExtractImageScansController { try { Files.deleteIfExists(path); } catch (IOException e) { - log.error("Failed to delete temporary image file: " + path, e); + log.error("Failed to delete temporary image file: {}", path, e); } }); @@ -205,7 +227,7 @@ public class ExtractImageScansController { try { Files.deleteIfExists(tempZipFile); } catch (IOException e) { - log.error("Failed to delete temporary zip file: " + tempZipFile, e); + log.error("Failed to delete temporary zip file: {}", tempZipFile, e); } } @@ -214,7 +236,7 @@ public class ExtractImageScansController { try { FileUtils.deleteDirectory(dir.toFile()); } catch (IOException e) { - log.error("Failed to delete temporary directory: " + dir, e); + log.error("Failed to delete temporary directory: {}", dir, e); } }); } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImagesController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImagesController.java index 24f64f8ea..0f2ea431c 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImagesController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImagesController.java @@ -29,7 +29,6 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; -import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; @@ -41,6 +40,7 @@ import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.MiscApi; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; +import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.ImageProcessingUtils; import stirling.software.common.util.WebResponseUtils; @@ -51,7 +51,7 @@ public class ExtractImagesController { private final CustomPDFDocumentFactory pdfDocumentFactory; - @AutoJobPostMapping(consumes = "multipart/form-data", value = "/extract-images") + @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/extract-images") @MultiFileResponse @Operation( summary = "Extract images from a PDF file", @@ -78,9 +78,7 @@ public class ExtractImagesController { // Set compression level zos.setLevel(Deflater.BEST_COMPRESSION); - String filename = - Filenames.toSimpleFileName(file.getOriginalFilename()) - .replaceFirst("[.][^.]+$", ""); + String filename = GeneralUtils.removeExtension(file.getOriginalFilename()); Set processedImages = new HashSet<>(); if (useMultithreading) { diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/FlattenController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/FlattenController.java index 94d1c1598..f0211c265 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/FlattenController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/FlattenController.java @@ -11,6 +11,7 @@ import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm; import org.apache.pdfbox.rendering.ImageType; 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.multipart.MultipartFile; @@ -25,7 +26,10 @@ import stirling.software.SPDF.config.swagger.StandardPdfResponse; import stirling.software.SPDF.model.api.misc.FlattenRequest; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.MiscApi; +import stirling.software.common.model.ApplicationProperties; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.ApplicationContextProvider; +import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.WebResponseUtils; @MiscApi @@ -35,7 +39,7 @@ public class FlattenController { private final CustomPDFDocumentFactory pdfDocumentFactory; - @AutoJobPostMapping(consumes = "multipart/form-data", value = "/flatten") + @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/flatten") @StandardPdfResponse @Operation( summary = "Flatten PDF form fields or full page", @@ -64,7 +68,23 @@ public class FlattenController { int numPages = document.getNumberOfPages(); for (int i = 0; i < numPages; i++) { try { - BufferedImage image = pdfRenderer.renderImageWithDPI(i, 300, ImageType.RGB); + BufferedImage image; + + // Use global maximum DPI setting, fallback to 300 if not set + int renderDpi = 300; // Default fallback + ApplicationProperties properties = + ApplicationContextProvider.getBean(ApplicationProperties.class); + if (properties != null && properties.getSystem() != null) { + renderDpi = properties.getSystem().getMaxDPI(); + } + + try { + image = pdfRenderer.renderImageWithDPI(i, renderDpi, ImageType.RGB); + } catch (OutOfMemoryError e) { + throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, renderDpi, e); + } catch (NegativeArraySizeException e) { + throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, renderDpi, e); + } PDPage page = new PDPage(); page.setMediaBox(document.getPage(i).getMediaBox()); newDocument.addPage(page); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/MetadataController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/MetadataController.java index 5f45d3419..449313d0b 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/MetadataController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/MetadataController.java @@ -1,8 +1,6 @@ package stirling.software.SPDF.controller.api.misc; import java.io.IOException; -import java.text.ParseException; -import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Map; import java.util.Map.Entry; @@ -10,6 +8,7 @@ import java.util.Map.Entry; import org.apache.pdfbox.cos.COSName; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocumentInformation; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.*; @@ -26,6 +25,9 @@ import stirling.software.SPDF.model.api.misc.MetadataRequest; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.MiscApi; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.service.PdfMetadataService; +import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.RegexPatternUtils; import stirling.software.common.util.WebResponseUtils; import stirling.software.common.util.propertyeditor.StringToMapPropertyEditor; @@ -51,7 +53,7 @@ public class MetadataController { binder.registerCustomEditor(Map.class, "allRequestParams", new StringToMapPropertyEditor()); } - @AutoJobPostMapping(consumes = "multipart/form-data", value = "/update-metadata") + @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/update-metadata") @StandardPdfResponse @Operation( summary = "Update metadata of a PDF file", @@ -137,37 +139,25 @@ public class MetadataController { && !key.contains("customValue")) { info.setCustomMetadataValue(key, entry.getValue()); } else if (key.contains("customKey")) { - int number = Integer.parseInt(key.replaceAll("\\D", "")); + int number = + Integer.parseInt( + RegexPatternUtils.getInstance() + .getNumericExtractionPattern() + .matcher(key) + .replaceAll("")); String customKey = entry.getValue(); String customValue = allRequestParams.get("customValue" + number); info.setCustomMetadataValue(customKey, customValue); } } } - if (creationDate != null && creationDate.length() > 0) { - Calendar creationDateCal = Calendar.getInstance(); - try { - creationDateCal.setTime( - new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").parse(creationDate)); - } catch (ParseException e) { - log.error("exception", e); - } - info.setCreationDate(creationDateCal); - } else { - info.setCreationDate(null); - } - if (modificationDate != null && modificationDate.length() > 0) { - Calendar modificationDateCal = Calendar.getInstance(); - try { - modificationDateCal.setTime( - new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").parse(modificationDate)); - } catch (ParseException e) { - log.error("exception", e); - } - info.setModificationDate(modificationDateCal); - } else { - info.setModificationDate(null); - } + // Set creation date using utility method + Calendar creationDateCal = PdfMetadataService.parseToCalendar(creationDate); + info.setCreationDate(creationDateCal); + + // Set modification date using utility method + Calendar modificationDateCal = PdfMetadataService.parseToCalendar(modificationDate); + info.setModificationDate(modificationDateCal); info.setCreator(creator); info.setKeywords(keywords); info.setAuthor(author); @@ -179,8 +169,8 @@ public class MetadataController { document.setDocumentInformation(info); return WebResponseUtils.pdfDocToWebResponse( document, - Filenames.toSimpleFileName(pdfFile.getOriginalFilename()) - .replaceFirst("[.][^.]+$", "") + GeneralUtils.removeExtension( + Filenames.toSimpleFileName(pdfFile.getOriginalFilename())) + "_metadata.pdf"); } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/OCRController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/OCRController.java index ce2a9c98f..f221e3a63 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/OCRController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/OCRController.java @@ -1,10 +1,14 @@ package stirling.software.SPDF.controller.api.misc; import java.awt.image.BufferedImage; -import java.io.*; +import java.io.File; +import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @@ -33,13 +37,8 @@ import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.MiscApi; import stirling.software.common.model.ApplicationProperties; import stirling.software.common.service.CustomPDFDocumentFactory; -import stirling.software.common.util.ExceptionUtils; -import stirling.software.common.util.ProcessExecutor; +import stirling.software.common.util.*; import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult; -import stirling.software.common.util.TempDirectory; -import stirling.software.common.util.TempFile; -import stirling.software.common.util.TempFileManager; -import stirling.software.common.util.WebResponseUtils; @MiscApi @Slf4j @@ -73,7 +72,7 @@ public class OCRController { .toList(); } - @AutoJobPostMapping(consumes = "multipart/form-data", value = "/ocr-pdf") + @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/ocr-pdf") @StandardPdfResponse @Operation( summary = "Process a PDF file with OCR", @@ -115,100 +114,82 @@ public class OCRController { // Use try-with-resources for proper temp file management try (TempFile tempInputFile = new TempFile(tempFileManager, ".pdf"); - TempFile tempOutputFile = new TempFile(tempFileManager, ".pdf")) { + TempFile tempOutputFile = new TempFile(tempFileManager, ".pdf"); + TempFile sidecarTextFile = sidecar ? new TempFile(tempFileManager, ".txt") : null) { inputFile.transferTo(tempInputFile.getFile()); - TempFile sidecarTextFile = null; + // Use OCRmyPDF if available (no fallback - error if it fails) + if (isOcrMyPdfEnabled()) { + processWithOcrMyPdf( + selectedLanguages, + sidecar, + deskew, + clean, + cleanFinal, + ocrType, + ocrRenderType, + removeImagesAfter, + tempInputFile.getPath(), + tempOutputFile.getPath(), + sidecarTextFile != null ? sidecarTextFile.getPath() : null); + log.info("OCRmyPDF processing completed successfully"); + } + // Use Tesseract only if OCRmyPDF is not available + else if (isTesseractEnabled()) { + processWithTesseract( + selectedLanguages, + ocrType, + tempInputFile.getPath(), + tempOutputFile.getPath()); + log.info("Tesseract processing completed successfully"); + } else { + throw ExceptionUtils.createOcrToolsUnavailableException(); + } - try { - // Use OCRmyPDF if available (no fallback - error if it fails) - if (isOcrMyPdfEnabled()) { - if (sidecar != null && sidecar) { - sidecarTextFile = new TempFile(tempFileManager, ".txt"); - } + // Read the processed PDF file + byte[] pdfBytes = Files.readAllBytes(tempOutputFile.getPath()); - processWithOcrMyPdf( - selectedLanguages, - sidecar, - deskew, - clean, - cleanFinal, - ocrType, - ocrRenderType, - removeImagesAfter, - tempInputFile.getPath(), - tempOutputFile.getPath(), - sidecarTextFile != null ? sidecarTextFile.getPath() : null); - log.info("OCRmyPDF processing completed successfully"); + // Return the OCR processed PDF as a response + String outputFilename = + GeneralUtils.removeExtension( + Filenames.toSimpleFileName(inputFile.getOriginalFilename())) + + "_OCR.pdf"; + + if (sidecar && sidecarTextFile != null) { + // Create a zip file containing both the PDF and the text file + String outputZipFilename = + GeneralUtils.removeExtension( + Filenames.toSimpleFileName(inputFile.getOriginalFilename())) + + "_OCR.zip"; + + try (TempFile tempZipFile = new TempFile(tempFileManager, ".zip"); + ZipOutputStream zipOut = + new ZipOutputStream(Files.newOutputStream(tempZipFile.getPath()))) { + + // Add PDF file to the zip + ZipEntry pdfEntry = new ZipEntry(outputFilename); + zipOut.putNextEntry(pdfEntry); + zipOut.write(pdfBytes); + zipOut.closeEntry(); + + // Add text file to the zip + ZipEntry txtEntry = new ZipEntry(outputFilename.replace(".pdf", ".txt")); + zipOut.putNextEntry(txtEntry); + Files.copy(sidecarTextFile.getPath(), zipOut); + zipOut.closeEntry(); + + zipOut.finish(); + + byte[] zipBytes = Files.readAllBytes(tempZipFile.getPath()); + + // Return the zip file containing both the PDF and the text file + return WebResponseUtils.bytesToWebResponse( + zipBytes, outputZipFilename, MediaType.APPLICATION_OCTET_STREAM); } - // Use Tesseract only if OCRmyPDF is not available - else if (isTesseractEnabled()) { - processWithTesseract( - selectedLanguages, - ocrType, - tempInputFile.getPath(), - tempOutputFile.getPath()); - log.info("Tesseract processing completed successfully"); - } else { - throw ExceptionUtils.createOcrToolsUnavailableException(); - } - - // Read the processed PDF file - byte[] pdfBytes = Files.readAllBytes(tempOutputFile.getPath()); - + } else { // Return the OCR processed PDF as a response - String outputFilename = - Filenames.toSimpleFileName(inputFile.getOriginalFilename()) - .replaceFirst("[.][^.]+$", "") - + "_OCR.pdf"; - - if (sidecar != null && sidecar && sidecarTextFile != null) { - // Create a zip file containing both the PDF and the text file - String outputZipFilename = - Filenames.toSimpleFileName(inputFile.getOriginalFilename()) - .replaceFirst("[.][^.]+$", "") - + "_OCR.zip"; - - try (TempFile tempZipFile = new TempFile(tempFileManager, ".zip"); - ZipOutputStream zipOut = - new ZipOutputStream( - Files.newOutputStream(tempZipFile.getPath()))) { - - // Add PDF file to the zip - ZipEntry pdfEntry = new ZipEntry(outputFilename); - zipOut.putNextEntry(pdfEntry); - zipOut.write(pdfBytes); - zipOut.closeEntry(); - - // Add text file to the zip - ZipEntry txtEntry = new ZipEntry(outputFilename.replace(".pdf", ".txt")); - zipOut.putNextEntry(txtEntry); - Files.copy(sidecarTextFile.getPath(), zipOut); - zipOut.closeEntry(); - - zipOut.finish(); - - byte[] zipBytes = Files.readAllBytes(tempZipFile.getPath()); - - // Return the zip file containing both the PDF and the text file - return WebResponseUtils.bytesToWebResponse( - zipBytes, outputZipFilename, MediaType.APPLICATION_OCTET_STREAM); - } - } else { - // Return the OCR processed PDF as a response - return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename); - } - - } finally { - // Clean up sidecar temp file if created - if (sidecarTextFile != null) { - try { - sidecarTextFile.close(); - } catch (Exception e) { - log.warn("Failed to close sidecar temp file", e); - } - } + return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename); } } } @@ -255,7 +236,7 @@ public class OCRController { if (cleanFinal != null && cleanFinal) { command.add("--clean-final"); } - if (ocrType != null && !"".equals(ocrType)) { + if (ocrType != null && !ocrType.isEmpty()) { if ("skip-text".equals(ocrType)) { command.add("--skip-text"); } else if ("force-ocr".equals(ocrType)) { @@ -336,7 +317,7 @@ public class OCRController { for (int pageNum = 0; pageNum < pageCount; pageNum++) { PDPage page = document.getPage(pageNum); - boolean hasText = false; + boolean hasText; // Check for existing text try (PDDocument tempDoc = new PDDocument()) { @@ -357,7 +338,24 @@ public class OCRController { if (shouldOcr) { // Convert page to image - BufferedImage image = pdfRenderer.renderImageWithDPI(pageNum, 300); + BufferedImage image; + + // Use global maximum DPI setting, fallback to 300 if not set + int renderDpi = 300; // Default fallback + if (applicationProperties != null + && applicationProperties.getSystem() != null) { + renderDpi = applicationProperties.getSystem().getMaxDPI(); + } + + try { + image = pdfRenderer.renderImageWithDPI(pageNum, renderDpi); + } catch (OutOfMemoryError e) { + throw ExceptionUtils.createOutOfMemoryDpiException( + pageNum + 1, renderDpi, e); + } catch (NegativeArraySizeException e) { + throw ExceptionUtils.createOutOfMemoryDpiException( + pageNum + 1, renderDpi, e); + } File imagePath = new File(tempImagesDir, String.format("page_%d.png", pageNum)); ImageIO.write(image, "png", imagePath); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/OverlayImageController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/OverlayImageController.java index 878d29af0..02f55a993 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/OverlayImageController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/OverlayImageController.java @@ -3,11 +3,11 @@ package stirling.software.SPDF.controller.api.misc; import java.io.IOException; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; -import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; @@ -17,6 +17,7 @@ import stirling.software.SPDF.model.api.misc.OverlayImageRequest; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.MiscApi; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.PdfUtils; import stirling.software.common.util.WebResponseUtils; @@ -27,7 +28,7 @@ public class OverlayImageController { private final CustomPDFDocumentFactory pdfDocumentFactory; - @AutoJobPostMapping(consumes = "multipart/form-data", value = "/add-image") + @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/add-image") @Operation( summary = "Overlay image onto a PDF file", description = @@ -49,9 +50,7 @@ public class OverlayImageController { return WebResponseUtils.bytesToWebResponse( result, - Filenames.toSimpleFileName(pdfFile.getOriginalFilename()) - .replaceFirst("[.][^.]+$", "") - + "_overlayed.pdf"); + GeneralUtils.generateFilename(pdfFile.getOriginalFilename(), "_overlayed.pdf")); } catch (IOException e) { log.error("Failed to add image to PDF", e); return new ResponseEntity<>(HttpStatus.BAD_REQUEST); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/PageNumbersController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/PageNumbersController.java index ec1825b15..3915ada1e 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/PageNumbersController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/PageNumbersController.java @@ -1,8 +1,10 @@ package stirling.software.SPDF.controller.api.misc; +import java.awt.Color; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.List; +import java.util.Locale; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; @@ -34,7 +36,7 @@ public class PageNumbersController { private final CustomPDFDocumentFactory pdfDocumentFactory; - @AutoJobPostMapping(value = "/add-page-numbers", consumes = "multipart/form-data") + @AutoJobPostMapping(value = "/add-page-numbers", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @StandardPdfResponse @Operation( summary = "Add page numbers to a PDF document", @@ -52,24 +54,27 @@ public class PageNumbersController { String customText = request.getCustomText(); float fontSize = request.getFontSize(); String fontType = request.getFontType(); + String fontColor = request.getFontColor(); + + Color color = Color.BLACK; + if (fontColor != null && !fontColor.trim().isEmpty()) { + try { + color = Color.decode(fontColor); + } catch (NumberFormatException e) { + color = Color.BLACK; + } + } PDDocument document = pdfDocumentFactory.load(file); - float marginFactor; - switch (customMargin.toLowerCase()) { - case "small": - marginFactor = 0.02f; - break; - case "large": - marginFactor = 0.05f; - break; - case "x-large": - marginFactor = 0.075f; - break; - case "medium": - default: - marginFactor = 0.035f; - break; - } + + float marginFactor = + switch (customMargin == null ? "" : customMargin.toLowerCase(Locale.ROOT)) { + case "small" -> 0.02f; + case "large" -> 0.05f; + case "x-large" -> 0.075f; + case "medium" -> 0.035f; + default -> 0.035f; + }; if (pagesToNumber == null || pagesToNumber.isEmpty()) { pagesToNumber = "all"; @@ -77,9 +82,17 @@ public class PageNumbersController { if (customText == null || customText.isEmpty()) { customText = "{n}"; } + + final String baseFilename = + Filenames.toSimpleFileName(file.getOriginalFilename()) + .replaceFirst("[.][^.]+$", ""); + List pagesToNumberList = GeneralUtils.parsePageList(pagesToNumber.split(","), document.getNumberOfPages()); + // Clamp position to 1..9 (1 = top-left, 9 = bottom-right) + int pos = Math.max(1, Math.min(9, position)); + for (int i : pagesToNumberList) { PDPage page = document.getPage(i); PDRectangle pageSize = page.getMediaBox(); @@ -90,68 +103,64 @@ public class PageNumbersController { .replace("{total}", String.valueOf(document.getNumberOfPages())) .replace( "{filename}", - Filenames.toSimpleFileName(file.getOriginalFilename()) - .replaceFirst("[.][^.]+$", "")); + GeneralUtils.removeExtension( + Filenames.toSimpleFileName( + file.getOriginalFilename()))); PDType1Font currentFont = - switch (fontType.toLowerCase()) { + switch (fontType == null ? "" : fontType.toLowerCase(Locale.ROOT)) { case "courier" -> new PDType1Font(Standard14Fonts.FontName.COURIER); case "times" -> new PDType1Font(Standard14Fonts.FontName.TIMES_ROMAN); default -> new PDType1Font(Standard14Fonts.FontName.HELVETICA); }; - float x, y; + // Text dimensions and font metrics + float textWidth = currentFont.getStringWidth(text) / 1000f * fontSize; + float ascent = currentFont.getFontDescriptor().getAscent() / 1000f * fontSize; + float descent = currentFont.getFontDescriptor().getDescent() / 1000f * fontSize; - if (position == 5) { - // Calculate text width and font metrics - float textWidth = currentFont.getStringWidth(text) / 1000 * fontSize; + // Derive column/row in range 1..3 (1 = left/top, 2 = center/middle, 3 = right/bottom) + int col = ((pos - 1) % 3) + 1; // 1 = left, 2 = center, 3 = right + int row = ((pos - 1) / 3) + 1; // 1 = top, 2 = middle, 3 = bottom - float ascent = currentFont.getFontDescriptor().getAscent() / 1000 * fontSize; - float descent = currentFont.getFontDescriptor().getDescent() / 1000 * fontSize; + // Anchor coordinates with margin + float leftX = pageSize.getLowerLeftX() + marginFactor * pageSize.getWidth(); + float midX = pageSize.getLowerLeftX() + pageSize.getWidth() / 2f; + float rightX = pageSize.getUpperRightX() - marginFactor * pageSize.getWidth(); - float centerX = pageSize.getLowerLeftX() + (pageSize.getWidth() / 2); - float centerY = pageSize.getLowerLeftY() + (pageSize.getHeight() / 2); + float botY = pageSize.getLowerLeftY() + marginFactor * pageSize.getHeight(); + float midY = pageSize.getLowerLeftY() + pageSize.getHeight() / 2f; + float topY = pageSize.getUpperRightY() - marginFactor * pageSize.getHeight(); - x = centerX - (textWidth / 2); - y = centerY - (ascent + descent) / 2; - } else { - int xGroup = (position - 1) % 3; - int yGroup = 2 - (position - 1) / 3; + // Horizontal alignment: left = anchor, center = centered, right = right-aligned + float x = + switch (col) { + case 1 -> leftX; + case 2 -> midX - textWidth / 2f; + default -> rightX - textWidth; + }; - x = - switch (xGroup) { - case 0 -> - pageSize.getLowerLeftX() - + marginFactor * pageSize.getWidth(); // left - case 1 -> - pageSize.getLowerLeftX() + (pageSize.getWidth() / 2); // center - default -> - pageSize.getUpperRightX() - - marginFactor * pageSize.getWidth(); // right - }; + // Vertical alignment (baseline!): + // top = align text top at topY, + // middle = optical middle using ascent/descent, + // bottom = baseline at botY + float y = + switch (row) { + case 1 -> topY - ascent; + case 2 -> midY - (ascent + descent) / 2f; + default -> botY; + }; - y = - switch (yGroup) { - case 0 -> - pageSize.getLowerLeftY() - + marginFactor * pageSize.getHeight(); // bottom - case 1 -> - pageSize.getLowerLeftY() + (pageSize.getHeight() / 2); // middle - default -> - pageSize.getUpperRightY() - - marginFactor * pageSize.getHeight(); // top - }; - } - - PDPageContentStream contentStream = + try (PDPageContentStream contentStream = new PDPageContentStream( - document, page, PDPageContentStream.AppendMode.APPEND, true, true); - contentStream.beginText(); - contentStream.setFont(currentFont, fontSize); - contentStream.newLineAtOffset(x, y); - contentStream.showText(text); - contentStream.endText(); - contentStream.close(); + document, page, PDPageContentStream.AppendMode.APPEND, true, true)) { + contentStream.beginText(); + contentStream.setFont(currentFont, fontSize); + contentStream.setNonStrokingColor(color); + contentStream.newLineAtOffset(x, y); + contentStream.showText(text); + contentStream.endText(); + } pageNumber++; } @@ -162,8 +171,7 @@ public class PageNumbersController { return WebResponseUtils.bytesToWebResponse( baos.toByteArray(), - Filenames.toSimpleFileName(file.getOriginalFilename()).replaceFirst("[.][^.]+$", "") - + "_numbersAdded.pdf", - MediaType.APPLICATION_PDF); + GeneralUtils.generateFilename( + file.getOriginalFilename(), "_page_numbers_added.pdf")); } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/PrintFileController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/PrintFileController.java index 9ab553a6f..ec213a77f 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/PrintFileController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/PrintFileController.java @@ -18,6 +18,7 @@ import org.apache.pdfbox.Loader; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.printing.PDFPageable; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; @@ -32,7 +33,7 @@ import stirling.software.common.annotations.api.MiscApi; public class PrintFileController { // TODO - // @AutoJobPostMapping(value = "/print-file", consumes = "multipart/form-data") + // @AutoJobPostMapping(value = "/print-file", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) // @Operation( // summary = "Prints PDF/Image file to a set printer", // description = @@ -62,9 +63,9 @@ public class PrintFileController { new IllegalArgumentException( "No matching printer found")); - log.info("Selected Printer: " + selectedService.getName()); + log.info("Selected Printer: {}", selectedService.getName()); - if ("application/pdf".equals(contentType)) { + if (MediaType.APPLICATION_PDF_VALUE.equals(contentType)) { PDDocument document = Loader.loadPDF(file.getBytes()); PrinterJob job = PrinterJob.getPrinterJob(); job.setPrintService(selectedService); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/RepairController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/RepairController.java index 19dac9108..02bcccc06 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/RepairController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/RepairController.java @@ -4,11 +4,11 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; -import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; @@ -20,6 +20,7 @@ import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.MiscApi; import stirling.software.common.model.api.PDFFile; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.ProcessExecutor; import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult; import stirling.software.common.util.TempFile; @@ -43,7 +44,7 @@ public class RepairController { return endpointConfiguration.isGroupEnabled("qpdf"); } - @AutoJobPostMapping(consumes = "multipart/form-data", value = "/repair") + @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/repair") @StandardPdfResponse @Operation( summary = "Repair a PDF file", @@ -121,11 +122,10 @@ public class RepairController { byte[] pdfBytes = pdfDocumentFactory.loadToBytes(tempOutputFile.getFile()); // Return the repaired PDF as a response - String outputFilename = - Filenames.toSimpleFileName(inputFile.getOriginalFilename()) - .replaceFirst("[.][^.]+$", "") - + "_repaired.pdf"; - return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename); + return WebResponseUtils.bytesToWebResponse( + pdfBytes, + GeneralUtils.generateFilename( + inputFile.getOriginalFilename(), "_repaired.pdf")); } } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ReplaceAndInvertColorController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ReplaceAndInvertColorController.java index 6fd1c9bf9..72820acb9 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ReplaceAndInvertColorController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ReplaceAndInvertColorController.java @@ -3,7 +3,6 @@ package stirling.software.SPDF.controller.api.misc; import java.io.IOException; import org.springframework.core.io.InputStreamResource; -import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; @@ -16,6 +15,8 @@ import stirling.software.SPDF.model.api.misc.ReplaceAndInvertColorRequest; import stirling.software.SPDF.service.misc.ReplaceAndInvertColorService; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.MiscApi; +import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.WebResponseUtils; @MiscApi @RequiredArgsConstructor @@ -23,13 +24,15 @@ public class ReplaceAndInvertColorController { private final ReplaceAndInvertColorService replaceAndInvertColorService; - @AutoJobPostMapping(consumes = "multipart/form-data", value = "/replace-invert-pdf") + @AutoJobPostMapping( + consumes = MediaType.MULTIPART_FORM_DATA_VALUE, + value = "/replace-invert-pdf") @Operation( summary = "Replace-Invert Color PDF", description = - "This endpoint accepts a PDF file and option of invert all colors or replace" - + " text and background colors. Input:PDF Output:PDF Type:SISO") - public ResponseEntity replaceAndInvertColor( + "This endpoint accepts a PDF file and provides options to invert all colors, replace" + + " text and background colors, or convert to CMYK color space for printing. Input:PDF Output:PDF Type:SISO") + public ResponseEntity replaceAndInvertColor( @ModelAttribute ReplaceAndInvertColorRequest request) throws IOException { InputStreamResource resource = @@ -41,9 +44,10 @@ public class ReplaceAndInvertColorController { request.getTextColor()); // Return the modified PDF as a downloadable file - return ResponseEntity.ok() - .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=inverted.pdf") - .contentType(MediaType.APPLICATION_PDF) - .body(resource); + String filename = + GeneralUtils.generateFilename( + request.getFileInput().getOriginalFilename(), "_inverted.pdf"); + return WebResponseUtils.bytesToWebResponse( + resource.getContentAsByteArray(), filename, MediaType.APPLICATION_PDF); } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ScannerEffectController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ScannerEffectController.java index 71d51b1f3..c4599dcae 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ScannerEffectController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ScannerEffectController.java @@ -21,7 +21,6 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; -import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.Valid; @@ -32,7 +31,11 @@ import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.model.api.misc.ScannerEffectRequest; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.MiscApi; +import stirling.software.common.model.ApplicationProperties; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.ApplicationContextProvider; +import stirling.software.common.util.ExceptionUtils; +import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.WebResponseUtils; @MiscApi @@ -48,7 +51,7 @@ public class ScannerEffectController { private static final int MAX_IMAGE_HEIGHT = 8192; private static final long MAX_IMAGE_PIXELS = 16_777_216; // 4096x4096 - @AutoJobPostMapping(value = "/scanner-effect", consumes = "multipart/form-data") + @AutoJobPostMapping(value = "/scanner-effect", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @Operation( summary = "Apply scanner effect to PDF", description = @@ -78,6 +81,22 @@ public class ScannerEffectController { int resolution = request.getResolution(); ScannerEffectRequest.Colorspace colorspace = request.getColorspace(); + // Validate and limit DPI to prevent excessive memory usage (respecting global limits) + int maxSafeDpi = 500; // Default maximum safe DPI + ApplicationProperties properties = + ApplicationContextProvider.getBean(ApplicationProperties.class); + if (properties != null && properties.getSystem() != null) { + maxSafeDpi = properties.getSystem().getMaxDPI(); + } + if (resolution > maxSafeDpi) { + throw ExceptionUtils.createIllegalArgumentException( + "error.dpiExceedsLimit", + "DPI value {0} exceeds maximum safe limit of {1}. High DPI values can cause" + + " memory issues and crashes. Please use a lower DPI value.", + resolution, + maxSafeDpi); + } + try (PDDocument document = pdfDocumentFactory.load(file)) { PDDocument outputDocument = new PDDocument(); PDFRenderer pdfRenderer = new PDFRenderer(document); @@ -114,7 +133,14 @@ public class ScannerEffectController { } // Render page to image with safe resolution - BufferedImage image = pdfRenderer.renderImageWithDPI(i, safeResolution); + BufferedImage image; + try { + image = pdfRenderer.renderImageWithDPI(i, safeResolution); + } catch (OutOfMemoryError e) { + throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, safeResolution, e); + } catch (NegativeArraySizeException e) { + throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, safeResolution, e); + } log.debug( "Processing page {} with dimensions {}x{} ({} pixels) at {}dpi", @@ -309,13 +335,10 @@ public class ScannerEffectController { outputDocument.save(outputStream); outputDocument.close(); - String outputFilename = - Filenames.toSimpleFileName(file.getOriginalFilename()) - .replaceFirst("[.][^.]+$", "") - + "_scanner_effect.pdf"; - return WebResponseUtils.bytesToWebResponse( - outputStream.toByteArray(), outputFilename, MediaType.APPLICATION_PDF); + outputStream.toByteArray(), + GeneralUtils.generateFilename( + file.getOriginalFilename(), "_scanner_effect.pdf")); } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ShowJavascript.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ShowJavascript.java index 6ed114616..29dc98350 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ShowJavascript.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ShowJavascript.java @@ -29,14 +29,15 @@ public class ShowJavascript { private final CustomPDFDocumentFactory pdfDocumentFactory; - @AutoJobPostMapping(consumes = "multipart/form-data", value = "/show-javascript") + @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/show-javascript") @JavaScriptResponse @Operation( summary = "Grabs all JS from a PDF and returns a single JS file with all code", description = "desc. Input:PDF Output:JS Type:SISO") public ResponseEntity extractHeader(@ModelAttribute PDFFile file) throws Exception { MultipartFile inputFile = file.getFileInput(); - String script = ""; + StringBuilder script = new StringBuilder(); + boolean foundScript = false; try (PDDocument document = pdfDocumentFactory.load(inputFile)) { @@ -53,28 +54,31 @@ public class ShowJavascript { PDActionJavaScript jsAction = entry.getValue(); String jsCodeStr = jsAction.getAction(); - script += - "// File: " - + Filenames.toSimpleFileName( - inputFile.getOriginalFilename()) - + ", Script: " - + name - + "\n" - + jsCodeStr - + "\n"; + if (jsCodeStr != null && !jsCodeStr.trim().isEmpty()) { + script.append("// File: ") + .append( + Filenames.toSimpleFileName( + inputFile.getOriginalFilename())) + .append(", Script: ") + .append(name) + .append("\n") + .append(jsCodeStr) + .append("\n"); + foundScript = true; + } } } } - if (script.isEmpty()) { + if (!foundScript) { script = - "PDF '" - + Filenames.toSimpleFileName(inputFile.getOriginalFilename()) - + "' does not contain Javascript"; + new StringBuilder("PDF '") + .append(Filenames.toSimpleFileName(inputFile.getOriginalFilename())) + .append("' does not contain Javascript"); } return WebResponseUtils.bytesToWebResponse( - script.getBytes(StandardCharsets.UTF_8), + script.toString().getBytes(StandardCharsets.UTF_8), Filenames.toSimpleFileName(inputFile.getOriginalFilename()) + ".js", MediaType.TEXT_PLAIN); } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/StampController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/StampController.java index 9f7005bcc..486cd2d12 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/StampController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/StampController.java @@ -2,6 +2,7 @@ package stirling.software.SPDF.controller.api.misc; import java.awt.*; import java.awt.image.BufferedImage; +import java.beans.PropertyEditorSupport; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -24,11 +25,13 @@ import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; import org.apache.pdfbox.pdmodel.graphics.state.PDExtendedGraphicsState; import org.apache.pdfbox.util.Matrix; import org.springframework.core.io.ClassPathResource; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.InitBinder; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; -import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; @@ -38,6 +41,8 @@ import stirling.software.SPDF.model.api.misc.AddStampRequest; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.MiscApi; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.RegexPatternUtils; import stirling.software.common.util.TempFile; import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @@ -49,7 +54,24 @@ public class StampController { private final CustomPDFDocumentFactory pdfDocumentFactory; private final TempFileManager tempFileManager; - @AutoJobPostMapping(consumes = "multipart/form-data", value = "/add-stamp") + /** + * Initialize data binder for multipart file uploads. This method registers a custom editor for + * MultipartFile to handle file uploads. It sets the MultipartFile to null if the uploaded file + * is empty. This is necessary to avoid binding errors when the file is not present. + */ + @InitBinder + public void initBinder(WebDataBinder binder) { + binder.registerCustomEditor( + MultipartFile.class, + new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue(null); + } + }); + } + + @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/add-stamp") @StandardPdfResponse @Operation( summary = "Add stamp to a PDF file", @@ -89,25 +111,14 @@ public class StampController { float overrideY = request.getOverrideY(); // New field for Y override String customColor = request.getCustomColor(); - float marginFactor; - - switch (request.getCustomMargin().toLowerCase()) { - case "small": - marginFactor = 0.02f; - break; - case "medium": - marginFactor = 0.035f; - break; - case "large": - marginFactor = 0.05f; - break; - case "x-large": - marginFactor = 0.075f; - break; - default: - marginFactor = 0.035f; - break; - } + float marginFactor = + switch (request.getCustomMargin().toLowerCase()) { + case "small" -> 0.02f; + case "medium" -> 0.035f; + case "large" -> 0.05f; + case "x-large" -> 0.075f; + default -> 0.035f; + }; // Load the input PDF PDDocument document = pdfDocumentFactory.load(pdfFile); @@ -160,11 +171,10 @@ public class StampController { contentStream.close(); } } + // Return the stamped PDF as a response return WebResponseUtils.pdfDocToWebResponse( document, - Filenames.toSimpleFileName(pdfFile.getOriginalFilename()) - .replaceFirst("[.][^.]+$", "") - + "_stamped.pdf"); + GeneralUtils.generateFilename(pdfFile.getOriginalFilename(), "_stamped.pdf")); } private void addTextStamp( @@ -181,42 +191,29 @@ public class StampController { float margin, String colorString) // Y override throws IOException { - String resourceDir = ""; + String resourceDir; PDFont font = new PDType1Font(Standard14Fonts.FontName.HELVETICA); - switch (alphabet) { - case "arabic": - resourceDir = "static/fonts/NotoSansArabic-Regular.ttf"; - break; - case "japanese": - resourceDir = "static/fonts/Meiryo.ttf"; - break; - case "korean": - resourceDir = "static/fonts/malgun.ttf"; - break; - case "chinese": - resourceDir = "static/fonts/SimSun.ttf"; - break; - case "thai": - resourceDir = "static/fonts/NotoSansThai-Regular.ttf"; - break; - case "roman": - default: - resourceDir = "static/fonts/NotoSans-Regular.ttf"; - break; - } + resourceDir = + switch (alphabet) { + case "arabic" -> "static/fonts/NotoSansArabic-Regular.ttf"; + case "japanese" -> "static/fonts/Meiryo.ttf"; + case "korean" -> "static/fonts/malgun.ttf"; + case "chinese" -> "static/fonts/SimSun.ttf"; + case "thai" -> "static/fonts/NotoSansThai-Regular.ttf"; + case "roman" -> "static/fonts/NotoSans-Regular.ttf"; + default -> "static/fonts/NotoSans-Regular.ttf"; + }; - if (!"".equals(resourceDir)) { - ClassPathResource classPathResource = new ClassPathResource(resourceDir); - String fileExtension = resourceDir.substring(resourceDir.lastIndexOf(".")); + ClassPathResource classPathResource = new ClassPathResource(resourceDir); + String fileExtension = resourceDir.substring(resourceDir.lastIndexOf(".")); - // Use TempFile with try-with-resources for automatic cleanup - try (TempFile tempFileWrapper = new TempFile(tempFileManager, fileExtension)) { - File tempFile = tempFileWrapper.getFile(); - try (InputStream is = classPathResource.getInputStream(); - FileOutputStream os = new FileOutputStream(tempFile)) { - IOUtils.copy(is, os); - font = PDType0Font.load(document, tempFile); - } + // Use TempFile with try-with-resources for automatic cleanup + try (TempFile tempFileWrapper = new TempFile(tempFileManager, fileExtension)) { + File tempFile = tempFileWrapper.getFile(); + try (InputStream is = classPathResource.getInputStream(); + FileOutputStream os = new FileOutputStream(tempFile)) { + IOUtils.copy(is, os); + font = PDType0Font.load(document, tempFile); } } @@ -238,7 +235,8 @@ public class StampController { PDRectangle pageSize = page.getMediaBox(); float x, y; // Split the stampText into multiple lines - String[] lines = stampText.split("\\r?\\n|\\\\n"); + String[] lines = + RegexPatternUtils.getInstance().getEscapedNewlinePattern().split(stampText); // Calculate dynamic line height based on font ascent and descent float ascent = font.getFontDescriptor().getAscent(); @@ -353,30 +351,30 @@ public class StampController { throws IOException { float actualWidth = (text != null) ? calculateTextWidth(text, font, fontSize) : contentWidth; - switch (position % 3) { + return switch (position % 3) { case 1: // Left - return pageSize.getLowerLeftX() + margin; + yield pageSize.getLowerLeftX() + margin; case 2: // Center - return (pageSize.getWidth() - actualWidth) / 2; + yield (pageSize.getWidth() - actualWidth) / 2; case 0: // Right - return pageSize.getUpperRightX() - actualWidth - margin; + yield pageSize.getUpperRightX() - actualWidth - margin; default: - return 0; - } + yield 0; + }; } private float calculatePositionY( PDRectangle pageSize, int position, float height, float margin) { - switch ((position - 1) / 3) { + return switch ((position - 1) / 3) { case 0: // Top - return pageSize.getUpperRightY() - height - margin; + yield pageSize.getUpperRightY() - height - margin; case 1: // Middle - return (pageSize.getHeight() - height) / 2; + yield (pageSize.getHeight() - height) / 2; case 2: // Bottom - return pageSize.getLowerLeftY() + margin; + yield pageSize.getLowerLeftY() + margin; default: - return 0; - } + yield 0; + }; } private float calculateTextWidth(String text, PDFont font, float fontSize) throws IOException { diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/UnlockPDFFormsController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/UnlockPDFFormsController.java index 32e81443f..faa98a7ea 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/UnlockPDFFormsController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/UnlockPDFFormsController.java @@ -10,6 +10,7 @@ import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.common.PDStream; import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm; import org.apache.pdfbox.pdmodel.interactive.form.PDField; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; @@ -23,6 +24,8 @@ import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.MiscApi; import stirling.software.common.model.api.PDFFile; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.RegexPatternUtils; import stirling.software.common.util.WebResponseUtils; @MiscApi @@ -34,7 +37,7 @@ public class UnlockPDFFormsController { this.pdfDocumentFactory = pdfDocumentFactory; } - @AutoJobPostMapping(consumes = "multipart/form-data", value = "/unlock-pdf-forms") + @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/unlock-pdf-forms") @StandardPdfResponse @Operation( summary = "Remove read-only property from form fields", @@ -63,13 +66,15 @@ public class UnlockPDFFormsController { COSBase xfaBase = acroForm.getCOSObject().getDictionaryObject(COSName.XFA); if (xfaBase != null) { try { + var accessReadOnlyPattern = + RegexPatternUtils.getInstance().getAccessReadOnlyPattern(); if (xfaBase instanceof COSStream xfaStream) { InputStream is = xfaStream.createInputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); is.transferTo(baos); String xml = baos.toString(StandardCharsets.UTF_8); - xml = xml.replaceAll("access\\s*=\\s*\"readOnly\"", "access=\"open\""); + xml = accessReadOnlyPattern.matcher(xml).replaceAll("access=\"open\""); PDStream newStream = new PDStream( @@ -89,9 +94,9 @@ public class UnlockPDFFormsController { String xml = baos.toString(StandardCharsets.UTF_8); xml = - xml.replaceAll( - "access\\s*=\\s*\"readOnly\"", - "access=\"open\""); + accessReadOnlyPattern + .matcher(xml) + .replaceAll("access=\"open\""); PDStream newStream = new PDStream( @@ -108,8 +113,8 @@ public class UnlockPDFFormsController { } } String mergedFileName = - file.getFileInput().getOriginalFilename().replaceFirst("[.][^.]+$", "") - + "_unlocked_forms.pdf"; + GeneralUtils.generateFilename( + file.getFileInput().getOriginalFilename(), "_unlocked_forms.pdf"); return WebResponseUtils.pdfDocToWebResponse( document, Filenames.toSimpleFileName(mergedFileName)); } catch (Exception e) { diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineController.java index bc1e77190..65e4ee1d0 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineController.java @@ -31,6 +31,7 @@ import stirling.software.SPDF.model.api.HandleDataRequest; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.PipelineApi; import stirling.software.common.service.PostHogService; +import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.WebResponseUtils; @PipelineApi @@ -73,7 +74,7 @@ public class PipelineController { try { List inputFiles = processor.generateInputFiles(files); - if (inputFiles == null || inputFiles.size() == 0) { + if (inputFiles == null || inputFiles.isEmpty()) { return null; } PipelineResult result = processor.runPipelineAgainstFiles(inputFiles, config); @@ -103,9 +104,8 @@ public class PipelineController { // Check if the filename already exists, and modify it if necessary if (filenameCount.containsKey(originalFilename)) { int count = filenameCount.get(originalFilename); - String baseName = originalFilename.replaceAll("\\.[^.]*$", ""); - String extension = originalFilename.replaceAll("^.*\\.", ""); - filename = baseName + "(" + count + ")." + extension; + assert originalFilename != null; + filename = GeneralUtils.generateFilename(originalFilename, "(" + count + ")"); filenameCount.put(originalFilename, count + 1); } else { filenameCount.put(originalFilename, 1); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineDirectoryProcessor.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineDirectoryProcessor.java index a3548ed49..63c08f619 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineDirectoryProcessor.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineDirectoryProcessor.java @@ -115,7 +115,7 @@ public class PipelineDirectoryProcessor { log.info("Handling directory: {}", dir); Path processingDir = createProcessingDirectory(dir); Optional jsonFileOptional = findJsonFile(dir); - if (!jsonFileOptional.isPresent()) { + if (jsonFileOptional.isEmpty()) { log.warn("No .JSON settings file found. No processing will happen for dir {}.", dir); return; } @@ -150,7 +150,7 @@ public class PipelineDirectoryProcessor { for (PipelineOperation operation : config.getOperations()) { validateOperation(operation); File[] files = collectFilesForProcessing(dir, jsonFile, operation); - if (files == null || files.length == 0) { + if (files.length == 0) { log.debug("No files detected for {} ", dir); return; } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java index 070d681e4..4ca863112 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java @@ -7,7 +7,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -106,7 +105,7 @@ public class PipelineProcessor { Map parameters = pipelineOperation.getParameters(); List inputFileTypes = apiDocService.getExtensionTypes(false, operation); if (inputFileTypes == null) { - inputFileTypes = new ArrayList(Arrays.asList("ALL")); + inputFileTypes = new ArrayList<>(List.of("ALL")); } if (!apiDocService.isValidOperation(operation, parameters)) { @@ -340,7 +339,7 @@ public class PipelineProcessor { } Path path = Paths.get(file.getAbsolutePath()); // debug statement - log.info("Reading file: " + path); + log.info("Reading file: {}", path); if (Files.exists(path)) { Resource fileResource = new ByteArrayResource(Files.readAllBytes(path)) { @@ -352,7 +351,7 @@ public class PipelineProcessor { }; outputFiles.add(fileResource); } else { - log.info("File not found: " + path); + log.info("File not found: {}", path); } } log.info("Files successfully loaded. Starting processing..."); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java index 4d2130e63..3b27492a6 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java @@ -58,13 +58,13 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.InitBinder; import org.springframework.web.bind.annotation.ModelAttribute; 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.micrometer.common.util.StringUtils; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -77,6 +77,7 @@ import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.service.ServerCertificateServiceInterface; import stirling.software.common.util.ExceptionUtils; +import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.WebResponseUtils; @RestController @@ -130,7 +131,7 @@ public class CertSignController { signature.setName(name); signature.setLocation(location); signature.setReason(reason); - signature.setSignDate(Calendar.getInstance()); + signature.setSignDate(Calendar.getInstance()); // PDFBox requires Calendar if (Boolean.TRUE.equals(showSignature)) { SignatureOptions signatureOptions = new SignatureOptions(); signatureOptions.setVisualSignature( @@ -198,6 +199,7 @@ public class CertSignController { "alias", privateKey, password.toCharArray(), new Certificate[] {cert}); break; case "PKCS12": + case "PFX": ks = KeyStore.getInstance("PKCS12"); ks.load(p12File.getInputStream(), password.toCharArray()); break; @@ -243,10 +245,10 @@ public class CertSignController { location, reason, showLogo); - return WebResponseUtils.baosToWebResponse( - baos, - Filenames.toSimpleFileName(pdf.getOriginalFilename()).replaceFirst("[.][^.]+$", "") - + "_signed.pdf"); + // Return the signed PDF + return WebResponseUtils.bytesToWebResponse( + baos.toByteArray(), + GeneralUtils.generateFilename(pdf.getOriginalFilename(), "_signed.pdf")); } private PrivateKey getPrivateKeyFromPEM(byte[] pemBytes, String password) diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java index d8c681e2e..ce1ce412b 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java @@ -3,12 +3,17 @@ package stirling.software.SPDF.controller.api.security; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.text.SimpleDateFormat; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.apache.pdfbox.cos.COSInputStream; import org.apache.pdfbox.cos.COSName; import org.apache.pdfbox.cos.COSString; +import org.apache.pdfbox.io.RandomAccessReadBuffer; import org.apache.pdfbox.pdmodel.*; import org.apache.pdfbox.pdmodel.common.PDMetadata; import org.apache.pdfbox.pdmodel.common.PDRectangle; @@ -38,8 +43,14 @@ import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlin import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineNode; import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm; import org.apache.pdfbox.pdmodel.interactive.form.PDField; +import org.apache.pdfbox.preflight.PreflightDocument; +import org.apache.pdfbox.preflight.ValidationResult; +import org.apache.pdfbox.preflight.exception.SyntaxValidationException; +import org.apache.pdfbox.preflight.exception.ValidationException; +import org.apache.pdfbox.preflight.parser.PreflightParser; import org.apache.pdfbox.text.PDFTextStripper; import org.apache.xmpbox.XMPMetadata; +import org.apache.xmpbox.schema.PDFAIdentificationSchema; import org.apache.xmpbox.xml.DomXmpParser; import org.apache.xmpbox.xml.XmpParsingException; import org.apache.xmpbox.xml.XmpSerializer; @@ -63,6 +74,7 @@ import stirling.software.common.annotations.api.SecurityApi; import stirling.software.common.model.api.PDFFile; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; +import stirling.software.common.util.RegexPatternUtils; import stirling.software.common.util.WebResponseUtils; @SecurityApi @@ -89,60 +101,147 @@ public class GetInfoOnPDF { } } - /** - * Generates structured summary data about the PDF highlighting its unique characteristics such - * as encryption status, permission restrictions, and standards compliance. - * - * @param document The PDF document to analyze - * @return An ObjectNode containing structured summary data - */ - private ObjectNode generatePDFSummaryData(PDDocument document) { - ObjectNode summaryData = objectMapper.createObjectNode(); - - // Check if encrypted - if (document.isEncrypted()) { - summaryData.put("encrypted", true); + public static boolean checkForStandard(PDDocument document, String standardKeyword) { + if ("PDF/A".equalsIgnoreCase(standardKeyword)) { + return getPdfAConformanceLevel(document) != null; } - // Check permissions - AccessPermission ap = document.getCurrentAccessPermission(); - ArrayNode restrictedPermissions = objectMapper.createArrayNode(); - - if (!ap.canAssembleDocument()) restrictedPermissions.add("document assembly"); - if (!ap.canExtractContent()) restrictedPermissions.add("content extraction"); - if (!ap.canExtractForAccessibility()) restrictedPermissions.add("accessibility extraction"); - if (!ap.canFillInForm()) restrictedPermissions.add("form filling"); - if (!ap.canModify()) restrictedPermissions.add("modification"); - if (!ap.canModifyAnnotations()) restrictedPermissions.add("annotation modification"); - if (!ap.canPrint()) restrictedPermissions.add("printing"); - - if (restrictedPermissions.size() > 0) { - summaryData.set("restrictedPermissions", restrictedPermissions); - summaryData.put("restrictedPermissionsCount", restrictedPermissions.size()); - } - - // Check standard compliance - if (checkForStandard(document, "PDF/A")) { - summaryData.put("standardCompliance", "PDF/A"); - summaryData.put("standardPurpose", "long-term archiving"); - } else if (checkForStandard(document, "PDF/X")) { - summaryData.put("standardCompliance", "PDF/X"); - summaryData.put("standardPurpose", "graphic exchange"); - } else if (checkForStandard(document, "PDF/UA")) { - summaryData.put("standardCompliance", "PDF/UA"); - summaryData.put("standardPurpose", "universal accessibility"); - } else if (checkForStandard(document, "PDF/E")) { - summaryData.put("standardCompliance", "PDF/E"); - summaryData.put("standardPurpose", "engineering workflows"); - } else if (checkForStandard(document, "PDF/VT")) { - summaryData.put("standardCompliance", "PDF/VT"); - summaryData.put("standardPurpose", "variable and transactional printing"); - } - - return summaryData; + return checkStandardInMetadata(document, standardKeyword); } - public static boolean checkForStandard(PDDocument document, String standardKeyword) { + public static String getPdfAConformanceLevel(PDDocument document) { + if (document == null || document.isEncrypted()) { + return null; + } + + return getPdfAVersionFromMetadata(document); + } + + private static String getPdfAVersionFromMetadata(PDDocument document) { + try { + PDMetadata pdMetadata = document.getDocumentCatalog().getMetadata(); + if (pdMetadata != null) { + try (COSInputStream metaStream = pdMetadata.createInputStream()) { + DomXmpParser domXmpParser = new DomXmpParser(); + XMPMetadata xmpMeta = domXmpParser.parse(metaStream); + + PDFAIdentificationSchema pdfId = xmpMeta.getPDFAIdentificationSchema(); + if (pdfId != null) { + Integer part = pdfId.getPart(); + String conformance = pdfId.getConformance(); + + if (part != null && conformance != null) { + return part + conformance.toUpperCase(Locale.ROOT); + } + } else { + try (COSInputStream rawStream = pdMetadata.createInputStream()) { + byte[] metadataBytes = rawStream.readAllBytes(); + String rawMetadata = new String(metadataBytes, StandardCharsets.UTF_8); + String extracted = extractPdfAVersionFromRawXml(rawMetadata); + if (extracted != null) { + return extracted; + } + } + } + } catch (XmpParsingException e) { + log.debug("XMP parsing failed, trying raw metadata search: {}", e.getMessage()); + try (COSInputStream metaStream = pdMetadata.createInputStream()) { + byte[] metadataBytes = metaStream.readAllBytes(); + String rawMetadata = new String(metadataBytes, StandardCharsets.UTF_8); + String extracted = extractPdfAVersionFromRawXml(rawMetadata); + if (extracted != null) { + return extracted; + } + } + } + } + } catch (Exception e) { + log.debug("Error reading PDF/A metadata: {}", e.getMessage()); + } + + return null; + } + + private static String extractPdfAVersionFromRawXml(String rawXml) { + if (rawXml == null || rawXml.isEmpty()) { + return null; + } + + try { + Pattern partPattern = RegexPatternUtils.getInstance().getPdfAidPartPattern(); + Pattern confPattern = RegexPatternUtils.getInstance().getPdfAidConformancePattern(); + + Matcher partMatcher = partPattern.matcher(rawXml); + Matcher confMatcher = confPattern.matcher(rawXml); + + if (partMatcher.find() && confMatcher.find()) { + String part = partMatcher.group(1); + String conformance = confMatcher.group(1).toUpperCase(Locale.ROOT); + return part + conformance; + } + } catch (Exception e) { + log.debug("Error parsing raw XMP for PDF/A version: {}", e.getMessage()); + } + + return null; + } + + private static boolean validatePdfAWithPreflight(PDDocument document, String version) { + if (document == null || document.isEncrypted()) { + return false; + } + + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + document.save(baos); + + try (RandomAccessReadBuffer source = new RandomAccessReadBuffer(baos.toByteArray())) { + PreflightParser parser = new PreflightParser(source); + + try (PDDocument parsedDocument = parser.parse()) { + if (!(parsedDocument instanceof PreflightDocument preflightDocument)) { + log.debug( + "Parsed document is not a PreflightDocument; unable to validate claimed PDF/A {}", + version); + return false; + } + + try { + ValidationResult result = preflightDocument.validate(); + if (!result.isValid() && log.isDebugEnabled()) { + log.debug( + "PDF/A validation found {} errors for claimed version {}", + result.getErrorsList().size(), + version); + int logged = 0; + for (ValidationResult.ValidationError error : result.getErrorsList()) { + log.debug( + " Error {}: {}", error.getErrorCode(), error.getDetails()); + if (++logged >= 5) { + break; + } + } + } + return result.isValid(); + } catch (ValidationException e) { + log.debug( + "Validation exception during PDF/A validation: {}", e.getMessage()); + } + } catch (SyntaxValidationException e) { + log.debug( + "Syntax validation failed during PDF/A validation: {}", e.getMessage()); + return false; + } + } + } catch (IOException e) { + log.debug("IOException during PDF/A validation: {}", e.getMessage()); + } catch (Exception e) { + log.debug("Unexpected error during PDF/A validation: {}", e.getMessage()); + } + + return false; + } + + private static boolean checkStandardInMetadata(PDDocument document, String standardKeyword) { // Check XMP Metadata try { PDMetadata pdMetadata = document.getDocumentCatalog().getMetadata(); @@ -185,13 +284,199 @@ public class GetInfoOnPDF { return false; } - @AutoJobPostMapping(consumes = "multipart/form-data", value = "/get-info-on-pdf") + /** + * Generates structured summary data about the PDF highlighting its unique characteristics such + * as encryption status, permission restrictions, and standards compliance. + * + * @param document The PDF document to analyze + * @return An ObjectNode containing structured summary data + */ + private ObjectNode generatePDFSummaryData( + PDDocument document, String pdfaConformanceLevel, Boolean pdfaValidationPassed) { + ObjectNode summaryData = objectMapper.createObjectNode(); + + // Check if encrypted + if (document.isEncrypted()) { + summaryData.put("encrypted", true); + } + + // Check permissions + AccessPermission ap = document.getCurrentAccessPermission(); + ArrayNode restrictedPermissions = objectMapper.createArrayNode(); + + if (!ap.canAssembleDocument()) restrictedPermissions.add("document assembly"); + if (!ap.canExtractContent()) restrictedPermissions.add("content extraction"); + if (!ap.canExtractForAccessibility()) restrictedPermissions.add("accessibility extraction"); + if (!ap.canFillInForm()) restrictedPermissions.add("form filling"); + if (!ap.canModify()) restrictedPermissions.add("modification"); + if (!ap.canModifyAnnotations()) restrictedPermissions.add("annotation modification"); + if (!ap.canPrint()) restrictedPermissions.add("printing"); + + if (!restrictedPermissions.isEmpty()) { + summaryData.set("restrictedPermissions", restrictedPermissions); + summaryData.put("restrictedPermissionsCount", restrictedPermissions.size()); + } + + // Check standard compliance + if (pdfaConformanceLevel != null) { + summaryData.put("standardCompliance", "PDF/A-" + pdfaConformanceLevel); + summaryData.put("standardPurpose", "long-term archiving"); + if (pdfaValidationPassed != null) { + summaryData.put("standardValidationPassed", pdfaValidationPassed); + } + } else if (checkForStandard(document, "PDF/X")) { + summaryData.put("standardCompliance", "PDF/X"); + summaryData.put("standardPurpose", "graphic exchange"); + } else if (checkForStandard(document, "PDF/UA")) { + summaryData.put("standardCompliance", "PDF/UA"); + summaryData.put("standardPurpose", "universal accessibility"); + } else if (checkForStandard(document, "PDF/E")) { + summaryData.put("standardCompliance", "PDF/E"); + summaryData.put("standardPurpose", "engineering workflows"); + } else if (checkForStandard(document, "PDF/VT")) { + summaryData.put("standardCompliance", "PDF/VT"); + summaryData.put("standardPurpose", "variable and transactional printing"); + } + + return summaryData; + } + + private static void setNodePermissions(PDDocument pdfBoxDoc, ObjectNode permissionsNode) { + AccessPermission ap = pdfBoxDoc.getCurrentAccessPermission(); + + permissionsNode.put("Document Assembly", getPermissionState(ap.canAssembleDocument())); + permissionsNode.put("Extracting Content", getPermissionState(ap.canExtractContent())); + permissionsNode.put( + "Extracting for accessibility", + getPermissionState(ap.canExtractForAccessibility())); + permissionsNode.put("Form Filling", getPermissionState(ap.canFillInForm())); + permissionsNode.put("Modifying", getPermissionState(ap.canModify())); + permissionsNode.put("Modifying annotations", getPermissionState(ap.canModifyAnnotations())); + permissionsNode.put("Printing", getPermissionState(ap.canPrint())); + } + + private static String getPermissionState(boolean state) { + return state ? "Allowed" : "Not Allowed"; + } + + public static String getPageOrientation(double width, double height) { + if (width > height) { + return "Landscape"; + } else if (height > width) { + return "Portrait"; + } else { + return "Square"; + } + } + + public static String getPageSize(float width, float height) { + // Define standard page sizes + Map standardSizes = new HashMap<>(); + standardSizes.put("Letter", PDRectangle.LETTER); + standardSizes.put("LEGAL", PDRectangle.LEGAL); + standardSizes.put("A0", PDRectangle.A0); + standardSizes.put("A1", PDRectangle.A1); + standardSizes.put("A2", PDRectangle.A2); + standardSizes.put("A3", PDRectangle.A3); + standardSizes.put("A4", PDRectangle.A4); + standardSizes.put("A5", PDRectangle.A5); + standardSizes.put("A6", PDRectangle.A6); + + for (Map.Entry entry : standardSizes.entrySet()) { + PDRectangle size = entry.getValue(); + if (isCloseToSize(width, height, size.getWidth(), size.getHeight())) { + return entry.getKey(); + } + } + return "Custom"; + } + + private static boolean isCloseToSize( + float width, float height, float standardWidth, float standardHeight) { + float tolerance = 1.0f; // You can adjust the tolerance as needed + return Math.abs(width - standardWidth) <= tolerance + && Math.abs(height - standardHeight) <= tolerance; + } + + private static void setDimensionInfo(ObjectNode dimensionInfo, float width, float height) { + float ppi = 72; // Points Per Inch + + float widthInInches = width / ppi; + float heightInInches = height / ppi; + + float widthInCm = widthInInches * 2.54f; + float heightInCm = heightInInches * 2.54f; + + dimensionInfo.put("Width (px)", String.format("%.2f", width)); + dimensionInfo.put("Height (px)", String.format("%.2f", height)); + dimensionInfo.put("Width (in)", String.format("%.2f", widthInInches)); + dimensionInfo.put("Height (in)", String.format("%.2f", heightInInches)); + dimensionInfo.put("Width (cm)", String.format("%.2f", widthInCm)); + dimensionInfo.put("Height (cm)", String.format("%.2f", heightInCm)); + } + + private static ArrayNode exploreStructureTree(List nodes) { + ArrayNode elementsArray = objectMapper.createArrayNode(); + if (nodes != null) { + for (Object obj : nodes) { + if (obj instanceof PDStructureNode node) { + ObjectNode elementNode = objectMapper.createObjectNode(); + + if (node instanceof PDStructureElement structureElement) { + elementNode.put("Type", structureElement.getStructureType()); + elementNode.put("Content", getContent(structureElement)); + + // Recursively explore child elements + ArrayNode childElements = exploreStructureTree(structureElement.getKids()); + if (!childElements.isEmpty()) { + elementNode.set("Children", childElements); + } + } + elementsArray.add(elementNode); + } + } + } + return elementsArray; + } + + private static String getContent(PDStructureElement structureElement) { + StringBuilder contentBuilder = new StringBuilder(); + + for (Object item : structureElement.getKids()) { + if (item instanceof COSString cosString) { + contentBuilder.append(cosString.getString()); + } else if (item instanceof PDStructureElement pdstructureelement) { + // For simplicity, we're handling only COSString and PDStructureElement here + // but a more comprehensive method would handle other types too + contentBuilder.append(getContent(pdstructureelement)); + } + } + + return contentBuilder.toString(); + } + + private static String formatDate(Calendar calendar) { + if (calendar != null) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + ZonedDateTime zonedDateTime = + ZonedDateTime.ofInstant(calendar.toInstant(), ZoneId.systemDefault()); + return zonedDateTime.format(formatter); + } else { + return null; + } + } + + @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/get-info-on-pdf") @JsonDataResponse @Operation(summary = "Summary here", description = "desc. Input:PDF Output:JSON Type:SISO") public ResponseEntity getPdfInfo(@ModelAttribute PDFFile request) throws IOException { MultipartFile inputFile = request.getFileInput(); boolean readonly = true; - try (PDDocument pdfBoxDoc = pdfDocumentFactory.load(inputFile, readonly); ) { + final String pagePrefix = "Page "; + final int prefixLength = pagePrefix.length(); + StringBuilder keyBuilder = new StringBuilder(prefixLength + 8); + keyBuilder.append(pagePrefix); + try (PDDocument pdfBoxDoc = pdfDocumentFactory.load(inputFile, readonly)) { ObjectMapper objectMapper = new ObjectMapper(); ObjectNode jsonOutput = objectMapper.createObjectNode(); @@ -220,9 +505,13 @@ public class GetInfoOnPDF { // Number of words, paragraphs, and images in the entire document String fullText = new PDFTextStripper().getText(pdfBoxDoc); - String[] words = fullText.split("\\s+"); + String[] words = RegexPatternUtils.getInstance().getWhitespacePattern().split(fullText); int wordCount = words.length; - int paragraphCount = fullText.split("\r\n|\r|\n").length; + int paragraphCount = + RegexPatternUtils.getInstance() + .getMultiFormatNewlinePattern() + .split(fullText) + .length; basicInfo.put("WordCount", wordCount); basicInfo.put("ParagraphCount", paragraphCount); // Number of characters in the entire document (including spaces and special characters) @@ -247,7 +536,6 @@ public class GetInfoOnPDF { docInfoNode.put("PDF version", pdfBoxDoc.getVersion()); docInfoNode.put("Trapped", info.getTrapped()); docInfoNode.put("Page Mode", getPageModeDescription(pageMode)); - ; PDAcroForm acroForm = pdfBoxDoc.getDocumentCatalog().getAcroForm(); @@ -259,9 +547,16 @@ public class GetInfoOnPDF { } jsonOutput.set("FormFields", formFieldsNode); + String pdfaConformanceLevel = getPdfAConformanceLevel(pdfBoxDoc); + Boolean pdfaValidationPassed = null; + if (pdfaConformanceLevel != null) { + pdfaValidationPassed = validatePdfAWithPreflight(pdfBoxDoc, pdfaConformanceLevel); + } + // Generate structured summary data about PDF characteristics - ObjectNode summaryData = generatePDFSummaryData(pdfBoxDoc); - if (summaryData != null && summaryData.size() > 0) { + ObjectNode summaryData = + generatePDFSummaryData(pdfBoxDoc, pdfaConformanceLevel, pdfaValidationPassed); + if (summaryData != null && !summaryData.isEmpty()) { jsonOutput.set("SummaryData", summaryData); } @@ -365,7 +660,7 @@ public class GetInfoOnPDF { log.error("exception", e); } - boolean isPdfACompliant = checkForStandard(pdfBoxDoc, "PDF/A"); + boolean isPdfACompliant = pdfaConformanceLevel != null; boolean isPdfXCompliant = checkForStandard(pdfBoxDoc, "PDF/X"); boolean isPdfECompliant = checkForStandard(pdfBoxDoc, "PDF/E"); boolean isPdfVTCompliant = checkForStandard(pdfBoxDoc, "PDF/VT"); @@ -382,6 +677,12 @@ public class GetInfoOnPDF { // development in 2021. compliancy.put("IsPDF/ACompliant", isPdfACompliant); + if (pdfaConformanceLevel != null) { + compliancy.put("PDF/AConformanceLevel", pdfaConformanceLevel); + if (pdfaValidationPassed != null) { + compliancy.put("IsPDF/AValidated", pdfaValidationPassed); + } + } compliancy.put("IsPDF/XCompliant", isPdfXCompliant); compliancy.put("IsPDF/ECompliant", isPdfECompliant); compliancy.put("IsPDF/VTCompliant", isPdfVTCompliant); @@ -458,7 +759,7 @@ public class GetInfoOnPDF { ObjectNode sizeInfo = objectMapper.createObjectNode(); - getDimensionInfo(sizeInfo, width, height); + setDimensionInfo(sizeInfo, width, height); sizeInfo.put("Standard Page", getPageSize(width, height)); pageInfo.set("Size", sizeInfo); @@ -544,11 +845,10 @@ public class GetInfoOnPDF { Set uniqueURIs = new HashSet<>(); // To store unique URIs for (PDAnnotation annotation : annotations) { - if (annotation instanceof PDAnnotationLink linkAnnotation) { - if (linkAnnotation.getAction() instanceof PDActionURI uriAction) { - String uri = uriAction.getURI(); - uniqueURIs.add(uri); // Add to set to ensure uniqueness - } + if (annotation instanceof PDAnnotationLink linkAnnotation + && linkAnnotation.getAction() instanceof PDActionURI uriAction) { + String uri = uriAction.getURI(); + uniqueURIs.add(uri); // Add to set to ensure uniqueness } } @@ -671,8 +971,10 @@ public class GetInfoOnPDF { } pageInfo.set("Multimedia", multimediaArray); + keyBuilder.setLength(prefixLength); + keyBuilder.append(pageNum + 1); - pageInfoParent.set("Page " + (pageNum + 1), pageInfo); + pageInfoParent.set(keyBuilder.toString(), pageInfo); } jsonOutput.set("BasicInfo", basicInfo); @@ -698,131 +1000,11 @@ public class GetInfoOnPDF { return null; } - private void setNodePermissions(PDDocument pdfBoxDoc, ObjectNode permissionsNode) { - AccessPermission ap = pdfBoxDoc.getCurrentAccessPermission(); - - permissionsNode.put("Document Assembly", getPermissionState(ap.canAssembleDocument())); - permissionsNode.put("Extracting Content", getPermissionState(ap.canExtractContent())); - permissionsNode.put( - "Extracting for accessibility", - getPermissionState(ap.canExtractForAccessibility())); - permissionsNode.put("Form Filling", getPermissionState(ap.canFillInForm())); - permissionsNode.put("Modifying", getPermissionState(ap.canModify())); - permissionsNode.put("Modifying annotations", getPermissionState(ap.canModifyAnnotations())); - permissionsNode.put("Printing", getPermissionState(ap.canPrint())); - } - - private String getPermissionState(boolean state) { - return state ? "Allowed" : "Not Allowed"; - } - - public String getPageOrientation(double width, double height) { - if (width > height) { - return "Landscape"; - } else if (height > width) { - return "Portrait"; - } else { - return "Square"; - } - } - - public String getPageSize(float width, float height) { - // Define standard page sizes - Map standardSizes = new HashMap<>(); - standardSizes.put("Letter", PDRectangle.LETTER); - standardSizes.put("LEGAL", PDRectangle.LEGAL); - standardSizes.put("A0", PDRectangle.A0); - standardSizes.put("A1", PDRectangle.A1); - standardSizes.put("A2", PDRectangle.A2); - standardSizes.put("A3", PDRectangle.A3); - standardSizes.put("A4", PDRectangle.A4); - standardSizes.put("A5", PDRectangle.A5); - standardSizes.put("A6", PDRectangle.A6); - - for (Map.Entry entry : standardSizes.entrySet()) { - PDRectangle size = entry.getValue(); - if (isCloseToSize(width, height, size.getWidth(), size.getHeight())) { - return entry.getKey(); - } - } - return "Custom"; - } - - private boolean isCloseToSize( - float width, float height, float standardWidth, float standardHeight) { - float tolerance = 1.0f; // You can adjust the tolerance as needed - return Math.abs(width - standardWidth) <= tolerance - && Math.abs(height - standardHeight) <= tolerance; - } - - public ObjectNode getDimensionInfo(ObjectNode dimensionInfo, float width, float height) { - float ppi = 72; // Points Per Inch - - float widthInInches = width / ppi; - float heightInInches = height / ppi; - - float widthInCm = widthInInches * 2.54f; - float heightInCm = heightInInches * 2.54f; - - dimensionInfo.put("Width (px)", String.format("%.2f", width)); - dimensionInfo.put("Height (px)", String.format("%.2f", height)); - dimensionInfo.put("Width (in)", String.format("%.2f", widthInInches)); - dimensionInfo.put("Height (in)", String.format("%.2f", heightInInches)); - dimensionInfo.put("Width (cm)", String.format("%.2f", widthInCm)); - dimensionInfo.put("Height (cm)", String.format("%.2f", heightInCm)); - return dimensionInfo; - } - - public ArrayNode exploreStructureTree(List nodes) { - ArrayNode elementsArray = objectMapper.createArrayNode(); - if (nodes != null) { - for (Object obj : nodes) { - if (obj instanceof PDStructureNode node) { - ObjectNode elementNode = objectMapper.createObjectNode(); - - if (node instanceof PDStructureElement structureElement) { - elementNode.put("Type", structureElement.getStructureType()); - elementNode.put("Content", getContent(structureElement)); - - // Recursively explore child elements - ArrayNode childElements = exploreStructureTree(structureElement.getKids()); - if (childElements.size() > 0) { - elementNode.set("Children", childElements); - } - } - elementsArray.add(elementNode); - } - } - } - return elementsArray; - } - - public String getContent(PDStructureElement structureElement) { - StringBuilder contentBuilder = new StringBuilder(); - - for (Object item : structureElement.getKids()) { - if (item instanceof COSString cosString) { - contentBuilder.append(cosString.getString()); - } else if (item instanceof PDStructureElement) { - // For simplicity, we're handling only COSString and PDStructureElement here - // but a more comprehensive method would handle other types too - contentBuilder.append(getContent((PDStructureElement) item)); - } - } - - return contentBuilder.toString(); - } - - private String formatDate(Calendar calendar) { - if (calendar != null) { - SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - return sdf.format(calendar.getTime()); - } else { - return null; - } - } - - private String getPageModeDescription(String pageMode) { - return pageMode != null ? pageMode.toString().replaceFirst("/", "") : "Unknown"; + private static String getPageModeDescription(String pageMode) { + if (pageMode == null) return "Unknown"; + return RegexPatternUtils.getInstance() + .getPageModePattern() + .matcher(pageMode) + .replaceFirst(""); } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/PasswordController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/PasswordController.java index 7475f94c4..3b8b99872 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/PasswordController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/PasswordController.java @@ -5,11 +5,11 @@ import java.io.IOException; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.encryption.AccessPermission; import org.apache.pdfbox.pdmodel.encryption.StandardProtectionPolicy; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; -import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; @@ -21,6 +21,7 @@ import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.SecurityApi; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; +import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.WebResponseUtils; @SecurityApi @@ -29,7 +30,7 @@ public class PasswordController { private final CustomPDFDocumentFactory pdfDocumentFactory; - @AutoJobPostMapping(consumes = "multipart/form-data", value = "/remove-password") + @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/remove-password") @StandardPdfResponse @Operation( summary = "Remove password from a PDF file", @@ -46,9 +47,8 @@ public class PasswordController { document.setAllSecurityToBeRemoved(true); return WebResponseUtils.pdfDocToWebResponse( document, - Filenames.toSimpleFileName(fileInput.getOriginalFilename()) - .replaceFirst("[.][^.]+$", "") - + "_password_removed.pdf"); + GeneralUtils.generateFilename( + fileInput.getOriginalFilename(), "_password_removed.pdf")); } catch (IOException e) { document.close(); ExceptionUtils.logException("password removal", e); @@ -56,7 +56,7 @@ public class PasswordController { } } - @AutoJobPostMapping(consumes = "multipart/form-data", value = "/add-password") + @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/add-password") @StandardPdfResponse @Operation( summary = "Add password to a PDF file", @@ -102,13 +102,10 @@ public class PasswordController { if ("".equals(ownerPassword) && "".equals(password)) return WebResponseUtils.pdfDocToWebResponse( document, - Filenames.toSimpleFileName(fileInput.getOriginalFilename()) - .replaceFirst("[.][^.]+$", "") - + "_permissions.pdf"); + GeneralUtils.generateFilename( + fileInput.getOriginalFilename(), "_permissions.pdf")); return WebResponseUtils.pdfDocToWebResponse( document, - Filenames.toSimpleFileName(fileInput.getOriginalFilename()) - .replaceFirst("[.][^.]+$", "") - + "_passworded.pdf"); + GeneralUtils.generateFilename(fileInput.getOriginalFilename(), "_passworded.pdf")); } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/RedactController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/RedactController.java index 5e481f93d..a7153cf84 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/RedactController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/RedactController.java @@ -1,19 +1,40 @@ package stirling.software.SPDF.controller.api.security; -import java.awt.*; +import java.awt.Color; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import org.apache.pdfbox.contentstream.operator.Operator; +import org.apache.pdfbox.cos.COSArray; +import org.apache.pdfbox.cos.COSBase; +import org.apache.pdfbox.cos.COSFloat; +import org.apache.pdfbox.cos.COSName; +import org.apache.pdfbox.cos.COSNumber; +import org.apache.pdfbox.cos.COSString; +import org.apache.pdfbox.pdfparser.PDFStreamParser; +import org.apache.pdfbox.pdfwriter.ContentStreamWriter; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.PDPageTree; +import org.apache.pdfbox.pdmodel.PDResources; import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.common.PDStream; +import org.apache.pdfbox.pdmodel.font.PDFont; +import org.apache.pdfbox.pdmodel.graphics.PDXObject; +import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.InitBinder; @@ -23,6 +44,8 @@ import org.springframework.web.multipart.MultipartFile; import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; +import lombok.AllArgsConstructor; +import lombok.Data; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -31,6 +54,9 @@ import stirling.software.SPDF.model.PDFText; import stirling.software.SPDF.model.api.security.ManualRedactPdfRequest; import stirling.software.SPDF.model.api.security.RedactPdfRequest; import stirling.software.SPDF.pdf.TextFinder; +import stirling.software.SPDF.utils.text.TextEncodingHelper; +import stirling.software.SPDF.utils.text.TextFinderUtils; +import stirling.software.SPDF.utils.text.WidthCalculator; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.SecurityApi; import stirling.software.common.model.api.security.RedactionArea; @@ -45,138 +71,417 @@ import stirling.software.common.util.propertyeditor.StringToArrayListPropertyEdi @RequiredArgsConstructor public class RedactController { + private static final float DEFAULT_TEXT_PADDING_MULTIPLIER = 0.6f; + private static final float PRECISION_THRESHOLD = 1e-3f; + private static final int FONT_SCALE_FACTOR = 1000; + + // Redaction box width reduction factor (10% reduction) + private static final float REDACTION_WIDTH_REDUCTION_FACTOR = 0.9f; + + // Text showing operators + private static final Set TEXT_SHOWING_OPERATORS = Set.of("Tj", "TJ", "'", "\""); + + private static final COSString EMPTY_COS_STRING = new COSString(""); + private final CustomPDFDocumentFactory pdfDocumentFactory; + private String removeFileExtension(String filename) { + return GeneralUtils.removeExtension(filename); + } + @InitBinder public void initBinder(WebDataBinder binder) { binder.registerCustomEditor( List.class, "redactions", new StringToArrayListPropertyEditor()); } - @AutoJobPostMapping(value = "/redact", consumes = "multipart/form-data") + @AutoJobPostMapping(value = "/redact", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @StandardPdfResponse @Operation( operationId = "redactPdfManual", summary = "Redacts areas and pages in a PDF document", description = - "This operation takes an input PDF file with a list of areas, page" - + " number(s)/range(s)/function(s) to redact. Input:PDF, Output:PDF," - + " Type:SISO") - public ResponseEntity redactPdfManual(@ModelAttribute ManualRedactPdfRequest request) + "This endpoint redacts content from a PDF file based on manually specified areas. " + + "Users can specify areas to redact and optionally convert the PDF to an image. " + + "Input:PDF Output:PDF Type:SISO") + public ResponseEntity redactPDF(@ModelAttribute ManualRedactPdfRequest request) throws IOException { + MultipartFile file = request.getFileInput(); List redactionAreas = request.getRedactions(); - PDDocument document = pdfDocumentFactory.load(file); + try (PDDocument document = pdfDocumentFactory.load(file)) { + PDPageTree allPages = document.getDocumentCatalog().getPages(); - PDPageTree allPages = document.getDocumentCatalog().getPages(); + redactPages(request, document, allPages); - redactPages(request, document, allPages); - redactAreas(redactionAreas, document, allPages); + redactAreas(redactionAreas, document, allPages); - if (Boolean.TRUE.equals(request.getConvertPDFToImage())) { - PDDocument convertedPdf = PdfUtils.convertPdfToPdfImage(document); - document.close(); - document = convertedPdf; + if (Boolean.TRUE.equals(request.getConvertPDFToImage())) { + try (PDDocument convertedPdf = PdfUtils.convertPdfToPdfImage(document)) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + convertedPdf.save(baos); + byte[] pdfContent = baos.toByteArray(); + + return WebResponseUtils.bytesToWebResponse( + pdfContent, + removeFileExtension( + Objects.requireNonNull( + Filenames.toSimpleFileName( + file.getOriginalFilename()))) + + "_redacted.pdf"); + } + } + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + document.save(baos); + byte[] pdfContent = baos.toByteArray(); + + return WebResponseUtils.bytesToWebResponse( + pdfContent, + removeFileExtension( + Objects.requireNonNull( + Filenames.toSimpleFileName(file.getOriginalFilename()))) + + "_redacted.pdf"); } - - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - document.save(baos); - document.close(); - - byte[] pdfContent = baos.toByteArray(); - return WebResponseUtils.bytesToWebResponse( - pdfContent, - Filenames.toSimpleFileName(file.getOriginalFilename()).replaceFirst("[.][^.]+$", "") - + "_redacted.pdf"); } private void redactAreas( List redactionAreas, PDDocument document, PDPageTree allPages) throws IOException { - // Group redaction areas by page + + if (redactionAreas == null || redactionAreas.isEmpty()) { + return; + } + Map> redactionsByPage = new HashMap<>(); - // Process and validate each redaction area for (RedactionArea redactionArea : redactionAreas) { + if (redactionArea.getPage() == null || redactionArea.getPage() <= 0 || redactionArea.getHeight() == null || redactionArea.getHeight() <= 0.0D || redactionArea.getWidth() == null - || redactionArea.getWidth() <= 0.0D) continue; + || redactionArea.getWidth() <= 0.0D) { + continue; + } - // Group by page number redactionsByPage .computeIfAbsent(redactionArea.getPage(), k -> new ArrayList<>()) .add(redactionArea); } - // Process each page only once for (Map.Entry> entry : redactionsByPage.entrySet()) { Integer pageNumber = entry.getKey(); List areasForPage = entry.getValue(); if (pageNumber > allPages.getCount()) { - continue; // Skip if page number is out of bounds + continue; // Skip if the page number is out of bounds } PDPage page = allPages.get(pageNumber - 1); - PDRectangle box = page.getBBox(); - // Create only one content stream per page - PDPageContentStream contentStream = + try (PDPageContentStream contentStream = new PDPageContentStream( - document, page, PDPageContentStream.AppendMode.APPEND, true, true); + document, page, PDPageContentStream.AppendMode.APPEND, true, true)) { - // Process all redactions for this page - for (RedactionArea redactionArea : areasForPage) { - Color redactColor = decodeOrDefault(redactionArea.getColor(), Color.BLACK); - contentStream.setNonStrokingColor(redactColor); + contentStream.saveGraphicsState(); + for (RedactionArea redactionArea : areasForPage) { + Color redactColor = decodeOrDefault(redactionArea.getColor()); - float x = redactionArea.getX().floatValue(); - float y = redactionArea.getY().floatValue(); - float width = redactionArea.getWidth().floatValue(); - float height = redactionArea.getHeight().floatValue(); + contentStream.setNonStrokingColor(redactColor); - contentStream.addRect(x, box.getHeight() - y - height, width, height); - contentStream.fill(); + float x = redactionArea.getX().floatValue(); + float y = redactionArea.getY().floatValue(); + float width = redactionArea.getWidth().floatValue(); + float height = redactionArea.getHeight().floatValue(); + + float pdfY = page.getBBox().getHeight() - y - height; + + contentStream.addRect(x, pdfY, width, height); + contentStream.fill(); + } + contentStream.restoreGraphicsState(); } - - contentStream.close(); } } private void redactPages( ManualRedactPdfRequest request, PDDocument document, PDPageTree allPages) throws IOException { - Color redactColor = decodeOrDefault(request.getPageRedactionColor(), Color.BLACK); + + Color redactColor = decodeOrDefault(request.getPageRedactionColor()); List pageNumbers = getPageNumbers(request, allPages.getCount()); + for (Integer pageNumber : pageNumbers) { + PDPage page = allPages.get(pageNumber); - PDPageContentStream contentStream = + try (PDPageContentStream contentStream = new PDPageContentStream( - document, page, PDPageContentStream.AppendMode.APPEND, true, true); - contentStream.setNonStrokingColor(redactColor); + document, page, PDPageContentStream.AppendMode.APPEND, true, true)) { + contentStream.setNonStrokingColor(redactColor); - PDRectangle box = page.getBBox(); + PDRectangle box = page.getBBox(); - contentStream.addRect(0, 0, box.getWidth(), box.getHeight()); - contentStream.fill(); - contentStream.close(); + contentStream.addRect(0, 0, box.getWidth(), box.getHeight()); + contentStream.fill(); + } } } - private Color decodeOrDefault(String hex, Color defaultColor) { - try { - if (hex != null && !hex.startsWith("#")) { - hex = "#" + hex; - } - return Color.decode(hex); - } catch (Exception e) { - return defaultColor; + private void redactFoundText( + PDDocument document, + List blocks, + float customPadding, + Color redactColor, + boolean isTextRemovalMode) + throws IOException { + + var allPages = document.getDocumentCatalog().getPages(); + + Map> blocksByPage = new HashMap<>(); + for (PDFText block : blocks) { + blocksByPage.computeIfAbsent(block.getPageIndex(), k -> new ArrayList<>()).add(block); } + + for (Map.Entry> entry : blocksByPage.entrySet()) { + Integer pageIndex = entry.getKey(); + List pageBlocks = entry.getValue(); + + if (pageIndex >= allPages.getCount()) { + continue; // Skip if page index is out of bounds + } + + var page = allPages.get(pageIndex); + try (PDPageContentStream contentStream = + new PDPageContentStream( + document, page, PDPageContentStream.AppendMode.APPEND, true, true)) { + + contentStream.saveGraphicsState(); + + try { + contentStream.setNonStrokingColor(redactColor); + PDRectangle pageBox = page.getBBox(); + + for (PDFText block : pageBlocks) { + float padding = + (block.getY2() - block.getY1()) * DEFAULT_TEXT_PADDING_MULTIPLIER + + customPadding; + + float originalWidth = block.getX2() - block.getX1(); + float boxWidth; + float boxX; + + // Only apply width reduction when text is actually being removed + if (isTextRemovalMode) { + // Calculate reduced width and center the box + boxWidth = + originalWidth + * REDACTION_WIDTH_REDUCTION_FACTOR; // 10% reduction + float widthReduction = originalWidth - boxWidth; + boxX = block.getX1() + (widthReduction / 2); // Center the reduced box + } else { + // Use original width for box-only redaction + boxWidth = originalWidth; + boxX = block.getX1(); + } + + contentStream.addRect( + boxX, + pageBox.getHeight() - block.getY2() - padding, + boxWidth, + block.getY2() - block.getY1() + 2 * padding); + } + + contentStream.fill(); + + } finally { + contentStream.restoreGraphicsState(); + } + } + } + } + + String createPlaceholderWithFont(String originalWord, PDFont font) { + if (originalWord == null || originalWord.isEmpty()) { + return originalWord; + } + + if (font != null && TextEncodingHelper.isFontSubset(font.getName())) { + try { + float originalWidth = safeGetStringWidth(font, originalWord) / FONT_SCALE_FACTOR; + return createAlternativePlaceholder(originalWord, originalWidth, font, 1.0f); + } catch (Exception e) { + log.debug( + "Subset font placeholder creation failed for {}: {}", + font.getName(), + e.getMessage()); + return ""; + } + } + + return " ".repeat(originalWord.length()); + } + + /** + * Enhanced placeholder creation using advanced width calculation. Incorporates font validation + * and sophisticated fallback strategies. + */ + String createPlaceholderWithWidth( + String originalWord, float targetWidth, PDFont font, float fontSize) { + if (originalWord == null || originalWord.isEmpty()) { + return originalWord; + } + + if (font == null || fontSize <= 0) { + return " ".repeat(originalWord.length()); + } + + try { + // Check font reliability before proceeding + if (!WidthCalculator.isWidthCalculationReliable(font)) { + log.debug( + "Font {} unreliable for width calculation, using simple placeholder", + font.getName()); + return " ".repeat(originalWord.length()); + } + + // Use enhanced subset font detection + if (TextEncodingHelper.isFontSubset(font.getName())) { + return createSubsetFontPlaceholder(originalWord, targetWidth, font, fontSize); + } + + // Enhanced space width calculation + float spaceWidth = WidthCalculator.calculateAccurateWidth(font, " ", fontSize); + + if (spaceWidth <= 0) { + return createAlternativePlaceholder(originalWord, targetWidth, font, fontSize); + } + + int spaceCount = Math.max(1, Math.round(targetWidth / spaceWidth)); + + // More conservative space limit based on original word characteristics + int maxSpaces = + Math.max( + originalWord.length() * 2, Math.round(targetWidth / spaceWidth * 1.5f)); + spaceCount = Math.min(spaceCount, maxSpaces); + + return " ".repeat(spaceCount); + + } catch (Exception e) { + log.debug("Enhanced placeholder creation failed: {}", e.getMessage()); + return createAlternativePlaceholder(originalWord, targetWidth, font, fontSize); + } + } + + private String createSubsetFontPlaceholder( + String originalWord, float targetWidth, PDFont font, float fontSize) { + try { + log.debug("Subset font {} - trying to find replacement characters", font.getName()); + String result = createAlternativePlaceholder(originalWord, targetWidth, font, fontSize); + + if (result.isEmpty()) { + log.debug( + "Subset font {} has no suitable replacement characters, using empty string", + font.getName()); + } + + return result; + + } catch (Exception e) { + log.debug("Subset font placeholder creation failed: {}", e.getMessage()); + return ""; + } + } + + private String createAlternativePlaceholder( + String originalWord, float targetWidth, PDFont font, float fontSize) { + try { + String[] alternatives = {" ", ".", "-", "_", "~", "°", "·"}; + + if (TextEncodingHelper.fontSupportsCharacter(font, " ")) { + float spaceWidth = safeGetStringWidth(font, " ") / FONT_SCALE_FACTOR * fontSize; + if (spaceWidth > 0) { + int spaceCount = Math.max(1, Math.round(targetWidth / spaceWidth)); + int maxSpaces = originalWord.length() * 2; + spaceCount = Math.min(spaceCount, maxSpaces); + log.debug("Using spaces for font {}", font.getName()); + return " ".repeat(spaceCount); + } + } + + for (String altChar : alternatives) { + if (" ".equals(altChar)) continue; // Already tried spaces + + try { + if (!TextEncodingHelper.fontSupportsCharacter(font, altChar)) { + continue; + } + + float charWidth = + safeGetStringWidth(font, altChar) / FONT_SCALE_FACTOR * fontSize; + if (charWidth > 0) { + int charCount = Math.max(1, Math.round(targetWidth / charWidth)); + int maxChars = originalWord.length() * 2; + charCount = Math.min(charCount, maxChars); + log.debug( + "Using character '{}' for width calculation but spaces for placeholder in font {}", + altChar, + font.getName()); + + return " ".repeat(charCount); + } + } catch (Exception e) { + } + } + + log.debug( + "All placeholder alternatives failed for font {}, using empty string", + font.getName()); + return ""; + + } catch (Exception e) { + log.debug("Alternative placeholder creation failed: {}", e.getMessage()); + return ""; + } + } + + void writeFilteredContentStream(PDDocument document, PDPage page, List tokens) + throws IOException { + + PDStream newStream = new PDStream(document); + + try { + try (var out = newStream.createOutputStream()) { + ContentStreamWriter writer = new ContentStreamWriter(out); + writer.writeTokens(tokens); + } + + page.setContents(newStream); + + } catch (IOException e) { + throw new IOException("Failed to write filtered content stream to page", e); + } + } + + Color decodeOrDefault(String hex) { + if (hex == null) { + return Color.BLACK; + } + + String colorString = hex.startsWith("#") ? hex : "#" + hex; + + try { + return Color.decode(colorString); + } catch (NumberFormatException e) { + return Color.BLACK; + } + } + + boolean isTextShowingOperator(String opName) { + return TEXT_SHOWING_OPERATORS.contains(opName); } private List getPageNumbers(ManualRedactPdfRequest request, int pagesCount) { @@ -189,82 +494,1198 @@ public class RedactController { return pageNumbers; } - @AutoJobPostMapping(value = "/auto-redact", consumes = "multipart/form-data") + @AutoJobPostMapping(value = "/auto-redact", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @StandardPdfResponse @Operation( + summary = "Redact PDF automatically", operationId = "redactPdfAuto", - summary = "Redacts listOfText in a PDF document", description = - "This operation takes an input PDF file and redacts the provided listOfText." - + " Input:PDF, Output:PDF, Type:SISO") - public ResponseEntity redactPdf(@ModelAttribute RedactPdfRequest request) - throws Exception { - MultipartFile file = request.getFileInput(); - String listOfTextString = request.getListOfText(); + "This endpoint automatically redacts text from a PDF file based on specified patterns. " + + "Users can provide text patterns to redact, with options for regex and whole word matching. " + + "Input:PDF Output:PDF Type:SISO") + public ResponseEntity redactPdf(@ModelAttribute RedactPdfRequest request) { + String[] listOfText = request.getListOfText().split("\n"); boolean useRegex = Boolean.TRUE.equals(request.getUseRegex()); boolean wholeWordSearchBool = Boolean.TRUE.equals(request.getWholeWordSearch()); - String colorString = request.getRedactColor(); - float customPadding = request.getCustomPadding(); - boolean convertPDFToImage = Boolean.TRUE.equals(request.getConvertPDFToImage()); - String[] listOfText = listOfTextString.split("\n"); - PDDocument document = pdfDocumentFactory.load(file); - - Color redactColor; - try { - if (!colorString.startsWith("#")) { - colorString = "#" + colorString; - } - redactColor = Color.decode(colorString); - } catch (NumberFormatException e) { - log.warn("Invalid color string provided. Using default color BLACK for redaction."); - redactColor = Color.BLACK; + if (listOfText.length == 0 || (listOfText.length == 1 && listOfText[0].trim().isEmpty())) { + throw new IllegalArgumentException("No text patterns provided for redaction"); } + PDDocument document = null; + PDDocument fallbackDocument = null; + + try { + if (request.getFileInput() == null) { + log.error("File input is null"); + throw new IllegalArgumentException("File input cannot be null"); + } + + document = pdfDocumentFactory.load(request.getFileInput()); + + if (document == null) { + log.error("Failed to load PDF document"); + throw new IllegalArgumentException("Failed to load PDF document"); + } + + Map> allFoundTextsByPage = + findTextToRedact(document, listOfText, useRegex, wholeWordSearchBool); + + int totalMatches = allFoundTextsByPage.values().stream().mapToInt(List::size).sum(); + log.info( + "Redaction scan: {} occurrences across {} pages (patterns={}, regex={}, wholeWord={})", + totalMatches, + allFoundTextsByPage.size(), + listOfText.length, + useRegex, + wholeWordSearchBool); + + if (allFoundTextsByPage.isEmpty()) { + log.info("No text found matching redaction patterns"); + byte[] originalContent; + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + document.save(baos); + originalContent = baos.toByteArray(); + } + + return WebResponseUtils.bytesToWebResponse( + originalContent, + removeFileExtension( + Objects.requireNonNull( + Filenames.toSimpleFileName( + request.getFileInput() + .getOriginalFilename()))) + + "_redacted.pdf"); + } + + boolean fallbackToBoxOnlyMode; + try { + fallbackToBoxOnlyMode = + performTextReplacement( + document, + allFoundTextsByPage, + listOfText, + useRegex, + wholeWordSearchBool); + } catch (Exception e) { + log.warn( + "Text replacement redaction failed, falling back to box-only mode: {}", + e.getMessage()); + fallbackToBoxOnlyMode = true; + } + + if (fallbackToBoxOnlyMode) { + log.warn( + "Font compatibility issues detected. Using box-only redaction mode for better reliability."); + + fallbackDocument = pdfDocumentFactory.load(request.getFileInput()); + + allFoundTextsByPage = + findTextToRedact( + fallbackDocument, listOfText, useRegex, wholeWordSearchBool); + + byte[] pdfContent = + finalizeRedaction( + fallbackDocument, + allFoundTextsByPage, + request.getRedactColor(), + request.getCustomPadding(), + request.getConvertPDFToImage(), + false); // Box-only mode, use original box sizes + + return WebResponseUtils.bytesToWebResponse( + pdfContent, + removeFileExtension( + Objects.requireNonNull( + Filenames.toSimpleFileName( + request.getFileInput() + .getOriginalFilename()))) + + "_redacted.pdf"); + } + + byte[] pdfContent = + finalizeRedaction( + document, + allFoundTextsByPage, + request.getRedactColor(), + request.getCustomPadding(), + request.getConvertPDFToImage(), + true); // Text removal mode, use reduced box sizes + + return WebResponseUtils.bytesToWebResponse( + pdfContent, + removeFileExtension( + Objects.requireNonNull( + Filenames.toSimpleFileName( + request.getFileInput().getOriginalFilename()))) + + "_redacted.pdf"); + + } catch (Exception e) { + log.error("Redaction operation failed: {}", e.getMessage(), e); + throw new RuntimeException("Failed to perform PDF redaction: " + e.getMessage(), e); + + } finally { + if (document != null) { + try { + if (fallbackDocument == null) { + document.close(); + } + } catch (IOException e) { + log.warn("Failed to close main document: {}", e.getMessage()); + } + } + + if (fallbackDocument != null) { + try { + fallbackDocument.close(); + } catch (IOException e) { + log.warn("Failed to close fallback document: {}", e.getMessage()); + } + } + } + } + + private Map> findTextToRedact( + PDDocument document, String[] listOfText, boolean useRegex, boolean wholeWordSearch) { + Map> allFoundTextsByPage = new HashMap<>(); + for (String text : listOfText) { text = text.trim(); - TextFinder textFinder = new TextFinder(text, useRegex, wholeWordSearchBool); - List foundTexts = textFinder.getTextLocations(document); - redactFoundText(document, foundTexts, customPadding, redactColor); + if (text.isEmpty()) continue; + + log.debug( + "Searching for text: '{}' (regex: {}, wholeWord: {})", + text, + useRegex, + wholeWordSearch); + + try { + TextFinder textFinder = new TextFinder(text, useRegex, wholeWordSearch); + textFinder.getText(document); + + List foundTexts = textFinder.getFoundTexts(); + log.debug("TextFinder found {} instances of '{}'", foundTexts.size(), text); + + for (PDFText found : foundTexts) { + allFoundTextsByPage + .computeIfAbsent(found.getPageIndex(), k -> new ArrayList<>()) + .add(found); + log.debug( + "Added match on page {} at ({},{},{},{}): '{}'", + found.getPageIndex(), + found.getX1(), + found.getY1(), + found.getX2(), + found.getY2(), + found.getText()); + } + } catch (Exception e) { + log.error("Error processing search term '{}': {}", text, e.getMessage()); + } } - if (convertPDFToImage) { - PDDocument convertedPdf = PdfUtils.convertPdfToPdfImage(document); - document.close(); - document = convertedPdf; + return allFoundTextsByPage; + } + + private boolean performTextReplacement( + PDDocument document, + Map> allFoundTextsByPage, + String[] listOfText, + boolean useRegex, + boolean wholeWordSearchBool) { + if (allFoundTextsByPage.isEmpty()) { + return false; + } + + if (detectCustomEncodingFonts(document)) { + log.warn( + "Custom encoded fonts detected (non-standard encodings / DictionaryEncoding / damaged fonts). " + + "Text replacement is unreliable for these fonts. Falling back to box-only redaction mode."); + return true; // signal caller to fall back + } + + try { + Set allSearchTerms = + Arrays.stream(listOfText) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .collect(Collectors.toSet()); + + int pageCount = 0; + for (PDPage page : document.getPages()) { + pageCount++; + List filteredTokens = + createTokensWithoutTargetText( + document, page, allSearchTerms, useRegex, wholeWordSearchBool); + writeFilteredContentStream(document, page, filteredTokens); + } + log.info("Successfully performed text replacement redaction on {} pages.", pageCount); + return false; + } catch (Exception e) { + log.error( + "Text replacement redaction failed due to font or encoding issues. " + + "Will fall back to box-only redaction mode. Error: {}", + e.getMessage()); + return true; + } + } + + private byte[] finalizeRedaction( + PDDocument document, + Map> allFoundTextsByPage, + String colorString, + float customPadding, + Boolean convertToImage, + boolean isTextRemovalMode) + throws IOException { + + List allFoundTexts = new ArrayList<>(); + for (List pageTexts : allFoundTextsByPage.values()) { + allFoundTexts.addAll(pageTexts); + } + + if (!allFoundTexts.isEmpty()) { + Color redactColor = decodeOrDefault(colorString); + + redactFoundText(document, allFoundTexts, customPadding, redactColor, isTextRemovalMode); + + cleanDocumentMetadata(document); + } + + if (Boolean.TRUE.equals(convertToImage)) { + try (PDDocument convertedPdf = PdfUtils.convertPdfToPdfImage(document)) { + cleanDocumentMetadata(convertedPdf); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + convertedPdf.save(baos); + byte[] out = baos.toByteArray(); + + log.info( + "Redaction finalized (image mode): {} pages ➜ {} KB", + convertedPdf.getNumberOfPages(), + out.length / 1024); + + return out; + } } ByteArrayOutputStream baos = new ByteArrayOutputStream(); document.save(baos); - document.close(); + byte[] out = baos.toByteArray(); - byte[] pdfContent = baos.toByteArray(); - return WebResponseUtils.bytesToWebResponse( - pdfContent, - Filenames.toSimpleFileName(file.getOriginalFilename()).replaceFirst("[.][^.]+$", "") - + "_redacted.pdf"); + log.info( + "Redaction finalized: {} pages ➜ {} KB", + document.getNumberOfPages(), + out.length / 1024); + + return out; } - private void redactFoundText( - PDDocument document, List blocks, float customPadding, Color redactColor) - throws IOException { - var allPages = document.getDocumentCatalog().getPages(); + private void cleanDocumentMetadata(PDDocument document) { + try { + var documentInfo = document.getDocumentInformation(); + if (documentInfo != null) { + documentInfo.setAuthor(null); + documentInfo.setSubject(null); + documentInfo.setKeywords(null); - for (PDFText block : blocks) { - var page = allPages.get(block.getPageIndex()); - PDPageContentStream contentStream = - new PDPageContentStream( - document, page, PDPageContentStream.AppendMode.APPEND, true, true); - contentStream.setNonStrokingColor(redactColor); - float padding = (block.getY2() - block.getY1()) * 0.3f + customPadding; - PDRectangle pageBox = page.getBBox(); - contentStream.addRect( - block.getX1(), - pageBox.getHeight() - block.getY1() - padding, - block.getX2() - block.getX1(), - block.getY2() - block.getY1() + 2 * padding); - contentStream.fill(); - contentStream.close(); + documentInfo.setModificationDate(java.util.Calendar.getInstance()); + + log.debug("Cleaned document metadata for security"); + } + + if (document.getDocumentCatalog() != null) { + try { + document.getDocumentCatalog().setMetadata(null); + } catch (Exception e) { + log.debug("Could not clear XMP metadata: {}", e.getMessage()); + } + } + + } catch (Exception e) { + log.warn("Failed to clean document metadata: {}", e.getMessage()); } } + + List createTokensWithoutTargetText( + PDDocument document, + PDPage page, + Set targetWords, + boolean useRegex, + boolean wholeWordSearch) + throws IOException { + + PDFStreamParser parser = new PDFStreamParser(page); + List tokens = new ArrayList<>(); + Object token; + while ((token = parser.parseNextToken()) != null) { + tokens.add(token); + } + + PDResources resources = page.getResources(); + if (resources != null) { + processPageXObjects(document, resources, targetWords, useRegex, wholeWordSearch); + } + + List textSegments = extractTextSegments(page, tokens); + + String completeText = buildCompleteText(textSegments); + + List matches = + findAllMatches(completeText, targetWords, useRegex, wholeWordSearch); + + return applyRedactionsToTokens(tokens, textSegments, matches); + } + + private void processPageXObjects( + PDDocument document, + PDResources resources, + Set targetWords, + boolean useRegex, + boolean wholeWordSearch) { + + for (COSName xobjName : resources.getXObjectNames()) { + try { + PDXObject xobj = resources.getXObject(xobjName); + if (xobj instanceof PDFormXObject formXObj) { + processFormXObject(document, formXObj, targetWords, useRegex, wholeWordSearch); + log.debug("Processed Form XObject: {}", xobjName.getName()); + } + } catch (Exception e) { + log.warn("Failed to process XObject {}: {}", xobjName.getName(), e.getMessage()); + } + } + } + + @Data + private static class GraphicsState { + private PDFont font = null; + private float fontSize = 0; + } + + @Data + @AllArgsConstructor + private static class TextSegment { + private int tokenIndex; + private String operatorName; + private String text; + private int startPos; + private int endPos; + private PDFont font; + private float fontSize; + } + + @Data + @AllArgsConstructor + private static class MatchRange { + private int startPos; + private int endPos; + } + + private List extractTextSegments(PDPage page, List tokens) { + + List segments = new ArrayList<>(); + int currentTextPos = 0; + GraphicsState graphicsState = new GraphicsState(); + PDResources resources = page.getResources(); + + for (int i = 0; i < tokens.size(); i++) { + Object currentToken = tokens.get(i); + + if (currentToken instanceof Operator op) { + String opName = op.getName(); + + if ("Tf".equals(opName) && i >= 2) { + try { + COSName fontName = (COSName) tokens.get(i - 2); + COSBase fontSizeBase = (COSBase) tokens.get(i - 1); + if (fontSizeBase instanceof COSNumber cosNumber) { + graphicsState.setFont(resources.getFont(fontName)); + graphicsState.setFontSize(cosNumber.floatValue()); + } + } catch (ClassCastException | IOException e) { + log.debug( + "Failed to extract font and font size from Tf operator: {}", + e.getMessage()); + } + } + + currentTextPos = + getCurrentTextPos( + tokens, segments, currentTextPos, graphicsState, i, opName); + } + } + + return segments; + } + + private String buildCompleteText(List segments) { + StringBuilder sb = new StringBuilder(); + for (TextSegment segment : segments) { + sb.append(segment.text); + } + return sb.toString(); + } + + private List findAllMatches( + String completeText, + Set targetWords, + boolean useRegex, + boolean wholeWordSearch) { + + // Use the new utility for creating optimized patterns + List patterns = + TextFinderUtils.createOptimizedSearchPatterns( + targetWords, useRegex, wholeWordSearch); + + return patterns.stream() + .flatMap( + pattern -> { + try { + return pattern.matcher(completeText).results(); + } catch (Exception e) { + log.debug( + "Pattern matching failed for pattern {}: {}", + pattern.pattern(), + e.getMessage()); + return java.util.stream.Stream.empty(); + } + }) + .map(matchResult -> new MatchRange(matchResult.start(), matchResult.end())) + .sorted(Comparator.comparingInt(MatchRange::getStartPos)) + .collect(Collectors.toList()); + } + + private List applyRedactionsToTokens( + List tokens, List textSegments, List matches) { + + long startTime = System.currentTimeMillis(); + + try { + List newTokens = new ArrayList<>(tokens); + + Map> matchesBySegment = new HashMap<>(); + for (MatchRange match : matches) { + for (int i = 0; i < textSegments.size(); i++) { + TextSegment segment = textSegments.get(i); + int overlapStart = Math.max(match.startPos, segment.startPos); + int overlapEnd = Math.min(match.endPos, segment.endPos); + if (overlapStart < overlapEnd) { + matchesBySegment.computeIfAbsent(i, k -> new ArrayList<>()).add(match); + } + } + } + + List tasks = new ArrayList<>(); + for (Map.Entry> entry : matchesBySegment.entrySet()) { + int segmentIndex = entry.getKey(); + List segmentMatches = entry.getValue(); + TextSegment segment = textSegments.get(segmentIndex); + + if ("Tj".equals(segment.operatorName) || "'".equals(segment.operatorName)) { + String newText = applyRedactionsToSegmentText(segment, segmentMatches); + try { + float adjustment = calculateWidthAdjustment(segment, segmentMatches); + tasks.add(new ModificationTask(segment, newText, adjustment)); + } catch (Exception e) { + log.debug( + "Width adjustment calculation failed for segment: {}", + e.getMessage()); + } + } else if ("TJ".equals(segment.operatorName)) { + tasks.add(new ModificationTask(segment, null, 0)); + } + } + + tasks.sort((a, b) -> Integer.compare(b.segment.tokenIndex, a.segment.tokenIndex)); + + for (ModificationTask task : tasks) { + List segmentMatches = + matchesBySegment.getOrDefault( + textSegments.indexOf(task.segment), Collections.emptyList()); + modifyTokenForRedaction( + newTokens, task.segment, task.newText, task.adjustment, segmentMatches); + } + + return newTokens; + + } finally { + long processingTime = System.currentTimeMillis() - startTime; + log.debug( + "Token redaction processing completed in {} ms for {} matches", + processingTime, + matches.size()); + } + } + + @Data + @AllArgsConstructor + private static class ModificationTask { + private TextSegment segment; + private String newText; // Only for Tj + private float adjustment; // Only for Tj + } + + private String applyRedactionsToSegmentText(TextSegment segment, List matches) { + String text = segment.getText(); + + if (segment.getFont() != null + && !TextEncodingHelper.isTextSegmentRemovable(segment.getFont(), text)) { + log.debug( + "Skipping text segment '{}' - font {} cannot process this text reliably", + text, + segment.getFont().getName()); + return text; // Return original text unchanged + } + + StringBuilder result = new StringBuilder(text); + + for (MatchRange match : matches) { + int segmentStart = Math.max(0, match.getStartPos() - segment.getStartPos()); + int segmentEnd = Math.min(text.length(), match.getEndPos() - segment.getStartPos()); + + if (segmentStart < text.length() && segmentEnd > segmentStart) { + String originalPart = text.substring(segmentStart, segmentEnd); + + if (segment.getFont() != null + && !TextEncodingHelper.isTextSegmentRemovable( + segment.getFont(), originalPart)) { + log.debug( + "Skipping text part '{}' within segment - cannot be processed reliably", + originalPart); + continue; // Skip this match, process others + } + + float originalWidth = 0; + if (segment.getFont() != null && segment.getFontSize() > 0) { + try { + originalWidth = + safeGetStringWidth(segment.getFont(), originalPart) + / FONT_SCALE_FACTOR + * segment.getFontSize(); + } catch (Exception e) { + log.debug( + "Failed to calculate original width for placeholder: {}", + e.getMessage()); + } + } + + String placeholder = + (originalWidth > 0) + ? createPlaceholderWithWidth( + originalPart, + originalWidth, + segment.getFont(), + segment.getFontSize()) + : createPlaceholderWithFont(originalPart, segment.getFont()); + + result.replace(segmentStart, segmentEnd, placeholder); + } + } + + return result.toString(); + } + + private float safeGetStringWidth(PDFont font, String text) { + if (font == null || text == null || text.isEmpty()) { + return 0; + } + + if (!WidthCalculator.isWidthCalculationReliable(font)) { + log.debug( + "Font {} flagged as unreliable for width calculation, using fallback", + font.getName()); + return calculateConservativeWidth(font, text); + } + + if (!TextEncodingHelper.canEncodeCharacters(font, text)) { + log.debug( + "Text cannot be encoded by font {}, using character-based fallback", + font.getName()); + return calculateCharacterBasedWidth(font, text); + } + + try { + float width = font.getStringWidth(text); + log.debug("Direct width calculation successful for '{}': {}", text, width); + return width; + + } catch (Exception e) { + log.debug( + "Direct width calculation failed for font {}: {}", + font.getName(), + e.getMessage()); + return calculateFallbackWidth(font, text); + } + } + + private float calculateCharacterBasedWidth(PDFont font, String text) { + try { + float totalWidth = 0; + for (int i = 0; i < text.length(); i++) { + String character = text.substring(i, i + 1); + try { + // Validate character encoding first + if (!TextEncodingHelper.fontSupportsCharacter(font, character)) { + totalWidth += font.getAverageFontWidth(); + continue; + } + + byte[] encoded = font.encode(character); + if (encoded.length > 0) { + int glyphCode = encoded[0] & 0xFF; + float glyphWidth = font.getWidth(glyphCode); + + // Try alternative width methods if primary fails + if (glyphWidth == 0) { + try { + glyphWidth = font.getWidthFromFont(glyphCode); + } catch (Exception e2) { + glyphWidth = font.getAverageFontWidth(); + } + } + + totalWidth += glyphWidth; + } else { + totalWidth += font.getAverageFontWidth(); + } + } catch (Exception e2) { + // Character processing failed, use average width + totalWidth += font.getAverageFontWidth(); + } + } + + log.debug("Character-based width calculation: {}", totalWidth); + return totalWidth; + + } catch (Exception e) { + log.debug("Character-based width calculation failed: {}", e.getMessage()); + return calculateConservativeWidth(font, text); + } + } + + private float calculateFallbackWidth(PDFont font, String text) { + try { + // Method 1: Font bounding box approach + if (font.getFontDescriptor() != null + && font.getFontDescriptor().getFontBoundingBox() != null) { + + PDRectangle bbox = font.getFontDescriptor().getFontBoundingBox(); + float avgCharWidth = bbox.getWidth() * 0.6f; // Conservative estimate + float fallbackWidth = text.length() * avgCharWidth; + + log.debug("Bounding box fallback width: {}", fallbackWidth); + return fallbackWidth; + } + + // Method 2: Average font width + try { + float avgWidth = font.getAverageFontWidth(); + if (avgWidth > 0) { + float fallbackWidth = text.length() * avgWidth; + log.debug("Average width fallback: {}", fallbackWidth); + return fallbackWidth; + } + } catch (Exception e2) { + log.debug("Average font width calculation failed: {}", e2.getMessage()); + } + + // Method 3: Conservative estimate based on font metrics + return calculateConservativeWidth(font, text); + + } catch (Exception e) { + log.debug("Fallback width calculation failed: {}", e.getMessage()); + return calculateConservativeWidth(font, text); + } + } + + private float calculateConservativeWidth(PDFont font, String text) { + float conservativeWidth = text.length() * 500f; + + log.debug( + "Conservative width estimate for font {} text '{}': {}", + font.getName(), + text, + conservativeWidth); + return conservativeWidth; + } + + private float calculateWidthAdjustment(TextSegment segment, List matches) { + try { + if (segment.getFont() == null || segment.getFontSize() <= 0) { + return 0; + } + + String fontName = segment.getFont().getName(); + if (fontName != null + && (fontName.contains("HOEPAP") || TextEncodingHelper.isFontSubset(fontName))) { + log.debug("Skipping width adjustment for problematic/subset font: {}", fontName); + return 0; + } + + float totalOriginal = 0; + float totalPlaceholder = 0; + + String text = segment.getText(); + + for (MatchRange match : matches) { + int segStart = Math.max(0, match.getStartPos() - segment.getStartPos()); + int segEnd = Math.min(text.length(), match.getEndPos() - segment.getStartPos()); + + if (segStart < text.length() && segEnd > segStart) { + String originalPart = text.substring(segStart, segEnd); + + float originalWidth = + safeGetStringWidth(segment.getFont(), originalPart) + / FONT_SCALE_FACTOR + * segment.getFontSize(); + + String placeholderPart = + createPlaceholderWithWidth( + originalPart, + originalWidth, + segment.getFont(), + segment.getFontSize()); + + float origUnits = safeGetStringWidth(segment.getFont(), originalPart); + float placeUnits = safeGetStringWidth(segment.getFont(), placeholderPart); + + float orig = (origUnits / FONT_SCALE_FACTOR) * segment.getFontSize(); + float place = (placeUnits / FONT_SCALE_FACTOR) * segment.getFontSize(); + + totalOriginal += orig; + totalPlaceholder += place; + } + } + + float adjustment = totalOriginal - totalPlaceholder; + + float maxReasonableAdjustment = + Math.max( + segment.getText().length() * segment.getFontSize() * 2, + totalOriginal * 1.5f // Allow up to 50% more than original width + ); + + if (Math.abs(adjustment) > maxReasonableAdjustment) { + log.debug( + "Width adjustment {} seems unreasonable for text length {}, capping to 0", + adjustment, + segment.getText().length()); + return 0; + } + + return adjustment; + } catch (Exception ex) { + log.debug("Width adjustment failed: {}", ex.getMessage()); + return 0; + } + } + + private void modifyTokenForRedaction( + List tokens, + TextSegment segment, + String newText, + float adjustment, + List matches) { + + if (segment.getTokenIndex() < 0 || segment.getTokenIndex() >= tokens.size()) { + return; + } + + Object token = tokens.get(segment.getTokenIndex()); + String operatorName = segment.getOperatorName(); + + try { + if (("Tj".equals(operatorName) || "'".equals(operatorName)) + && token instanceof COSString) { + + if (Math.abs(adjustment) < PRECISION_THRESHOLD) { + if (newText.isEmpty()) { + tokens.set(segment.getTokenIndex(), EMPTY_COS_STRING); + } else { + tokens.set(segment.getTokenIndex(), new COSString(newText)); + } + } else { + COSArray newArray = new COSArray(); + newArray.add(new COSString(newText)); + if (segment.getFontSize() > 0) { + + float kerning = (-adjustment / segment.getFontSize()) * FONT_SCALE_FACTOR; + + newArray.add(new COSFloat(kerning)); + } + tokens.set(segment.getTokenIndex(), newArray); + + int operatorIndex = segment.getTokenIndex() + 1; + if (operatorIndex < tokens.size() + && tokens.get(operatorIndex) instanceof Operator op + && op.getName().equals(operatorName)) { + tokens.set(operatorIndex, Operator.getOperator("TJ")); + } + } + } else if ("TJ".equals(operatorName) && token instanceof COSArray) { + COSArray newArray = createRedactedTJArray((COSArray) token, segment, matches); + tokens.set(segment.getTokenIndex(), newArray); + } + } catch (Exception e) { + log.debug( + "Token modification failed for segment at index {}: {}", + segment.getTokenIndex(), + e.getMessage()); + } + } + + private COSArray createRedactedTJArray( + COSArray originalArray, TextSegment segment, List matches) { + try { + COSArray newArray = new COSArray(); + int textOffsetInSegment = 0; + + for (COSBase element : originalArray) { + if (element instanceof COSString cosString) { + String originalText = cosString.getString(); + + if (segment.getFont() != null + && !TextEncodingHelper.isTextSegmentRemovable( + segment.getFont(), originalText)) { + log.debug( + "Skipping TJ text part '{}' - cannot be processed reliably with font {}", + originalText, + segment.getFont().getName()); + newArray.add(element); // Keep original unchanged + textOffsetInSegment += originalText.length(); + continue; + } + + StringBuilder newText = new StringBuilder(originalText); + boolean modified = false; + + for (MatchRange match : matches) { + int stringStartInPage = segment.getStartPos() + textOffsetInSegment; + int stringEndInPage = stringStartInPage + originalText.length(); + + int overlapStart = Math.max(match.getStartPos(), stringStartInPage); + int overlapEnd = Math.min(match.getEndPos(), stringEndInPage); + + if (overlapStart < overlapEnd) { + int redactionStartInString = overlapStart - stringStartInPage; + int redactionEndInString = overlapEnd - stringStartInPage; + if (redactionStartInString >= 0 + && redactionEndInString <= originalText.length()) { + String originalPart = + originalText.substring( + redactionStartInString, redactionEndInString); + + if (segment.getFont() != null + && !TextEncodingHelper.isTextSegmentRemovable( + segment.getFont(), originalPart)) { + log.debug( + "Skipping TJ text part '{}' - cannot be redacted reliably", + originalPart); + continue; // Skip this redaction, keep original text + } + + modified = true; + float originalWidth = 0; + if (segment.getFont() != null && segment.getFontSize() > 0) { + try { + originalWidth = + safeGetStringWidth(segment.getFont(), originalPart) + / FONT_SCALE_FACTOR + * segment.getFontSize(); + } catch (Exception e) { + log.debug( + "Failed to calculate original width for TJ placeholder: {}", + e.getMessage()); + } + } + + String placeholder = + (originalWidth > 0) + ? createPlaceholderWithWidth( + originalPart, + originalWidth, + segment.getFont(), + segment.getFontSize()) + : createPlaceholderWithFont( + originalPart, segment.getFont()); + + newText.replace( + redactionStartInString, redactionEndInString, placeholder); + } + } + } + + String modifiedString = newText.toString(); + newArray.add(new COSString(modifiedString)); + + if (modified && segment.getFont() != null && segment.getFontSize() > 0) { + try { + float originalWidth = + safeGetStringWidth(segment.getFont(), originalText) + / FONT_SCALE_FACTOR + * segment.getFontSize(); + float modifiedWidth = + safeGetStringWidth(segment.getFont(), modifiedString) + / FONT_SCALE_FACTOR + * segment.getFontSize(); + float adjustment = originalWidth - modifiedWidth; + if (Math.abs(adjustment) > PRECISION_THRESHOLD) { + float kerning = + (-adjustment / segment.getFontSize()) + * FONT_SCALE_FACTOR + * 1.10f; + + newArray.add(new COSFloat(kerning)); + } + } catch (Exception e) { + log.debug( + "Width adjustment calculation failed for segment: {}", + e.getMessage()); + } + } + + textOffsetInSegment += originalText.length(); + } else { + newArray.add(element); + } + } + return newArray; + } catch (Exception e) { + return originalArray; + } + } + + private String extractTextFromToken(Object token, String operatorName) { + return switch (operatorName) { + case "Tj", "'" -> { + if (token instanceof COSString cosString) { + yield cosString.getString(); + } + yield ""; + } + case "TJ" -> { + if (token instanceof COSArray cosArray) { + StringBuilder sb = new StringBuilder(); + for (COSBase element : cosArray) { + if (element instanceof COSString cosString) { + sb.append(cosString.getString()); + } + } + yield sb.toString(); + } + yield ""; + } + default -> ""; + }; + } + + private boolean detectCustomEncodingFonts(PDDocument document) { + try { + var documentCatalog = document.getDocumentCatalog(); + if (documentCatalog == null) { + return false; + } + + int totalFonts = 0; + int customEncodedFonts = 0; + int subsetFonts = 0; + int unreliableFonts = 0; + + for (PDPage page : document.getPages()) { + if (TextFinderUtils.hasProblematicFonts(page)) { + log.debug("Page contains fonts flagged as problematic by TextFinderUtils"); + } + + PDResources resources = page.getResources(); + if (resources == null) { + continue; + } + + for (COSName fontName : resources.getFontNames()) { + try { + PDFont font = resources.getFont(fontName); + if (font != null) { + totalFonts++; + + // Enhanced analysis using helper classes + boolean isSubset = TextEncodingHelper.isFontSubset(font.getName()); + boolean hasCustomEncoding = TextEncodingHelper.hasCustomEncoding(font); + boolean isReliable = WidthCalculator.isWidthCalculationReliable(font); + boolean canCalculateWidths = + TextEncodingHelper.canCalculateBasicWidths(font); + + if (isSubset) { + subsetFonts++; + } + + if (hasCustomEncoding) { + customEncodedFonts++; + log.debug("Font {} has custom encoding", font.getName()); + } + + if (!isReliable || !canCalculateWidths) { + unreliableFonts++; + log.debug( + "Font {} flagged as unreliable: reliable={}, canCalculateWidths={}", + font.getName(), + isReliable, + canCalculateWidths); + } + + if (!TextFinderUtils.validateFontReliability(font)) { + log.debug( + "Font {} failed comprehensive reliability check", + font.getName()); + } + } + } catch (Exception e) { + log.debug( + "Font loading/analysis failed for {}: {}", + fontName.getName(), + e.getMessage()); + customEncodedFonts++; + unreliableFonts++; + totalFonts++; + } + } + } + + log.info( + "Enhanced font analysis: {}/{} custom encoding, {}/{} subset, {}/{} unreliable fonts", + customEncodedFonts, + totalFonts, + subsetFonts, + totalFonts, + unreliableFonts, + totalFonts); + + // Consider document problematic if we have custom encodings or unreliable fonts + return customEncodedFonts > 0 || unreliableFonts > 0; + + } catch (Exception e) { + log.warn("Enhanced font detection analysis failed: {}", e.getMessage()); + return true; // Assume problematic if analysis fails + } + } + + private void processFormXObject( + PDDocument document, + PDFormXObject formXObject, + Set targetWords, + boolean useRegex, + boolean wholeWordSearch) { + + try { + PDResources xobjResources = formXObject.getResources(); + if (xobjResources == null) { + return; + } + + for (COSName xobjName : xobjResources.getXObjectNames()) { + PDXObject nestedXObj = xobjResources.getXObject(xobjName); + if (nestedXObj instanceof PDFormXObject nestedFormXObj) { + processFormXObject( + document, nestedFormXObj, targetWords, useRegex, wholeWordSearch); + } + } + + PDFStreamParser parser = new PDFStreamParser(formXObject); + List tokens = new ArrayList<>(); + Object token; + while ((token = parser.parseNextToken()) != null) { + tokens.add(token); + } + + List textSegments = extractTextSegmentsFromXObject(xobjResources, tokens); + String completeText = buildCompleteText(textSegments); + + List matches = + findAllMatches(completeText, targetWords, useRegex, wholeWordSearch); + + if (!matches.isEmpty()) { + List redactedTokens = + applyRedactionsToTokens(tokens, textSegments, matches); + writeRedactedContentToXObject(document, formXObject, redactedTokens); + log.debug("Processed {} redactions in Form XObject", matches.size()); + } + + } catch (Exception e) { + log.warn("Failed to process Form XObject: {}", e.getMessage()); + } + } + + private List extractTextSegmentsFromXObject( + PDResources resources, List tokens) { + List segments = new ArrayList<>(); + int currentTextPos = 0; + GraphicsState graphicsState = new GraphicsState(); + + for (int i = 0; i < tokens.size(); i++) { + Object currentToken = tokens.get(i); + + if (currentToken instanceof Operator op) { + String opName = op.getName(); + + if ("Tf".equals(opName) && i >= 2) { + try { + COSName fontName = (COSName) tokens.get(i - 2); + COSBase fontSizeBase = (COSBase) tokens.get(i - 1); + if (fontSizeBase instanceof COSNumber cosNumber) { + graphicsState.setFont(resources.getFont(fontName)); + graphicsState.setFontSize(cosNumber.floatValue()); + } + } catch (ClassCastException | IOException e) { + log.debug("Font extraction failed in XObject: {}", e.getMessage()); + } + } + + currentTextPos = + getCurrentTextPos( + tokens, segments, currentTextPos, graphicsState, i, opName); + } + } + + return segments; + } + + private int getCurrentTextPos( + List tokens, + List segments, + int currentTextPos, + GraphicsState graphicsState, + int i, + String opName) { + if (isTextShowingOperator(opName) && i > 0) { + String textContent = extractTextFromToken(tokens.get(i - 1), opName); + if (!textContent.isEmpty()) { + segments.add( + new TextSegment( + i - 1, + opName, + textContent, + currentTextPos, + currentTextPos + textContent.length(), + graphicsState.font, + graphicsState.fontSize)); + currentTextPos += textContent.length(); + } + } + return currentTextPos; + } + + private void writeRedactedContentToXObject( + PDDocument document, PDFormXObject formXObject, List redactedTokens) + throws IOException { + + PDStream newStream = new PDStream(document); + + try (var out = newStream.createOutputStream()) { + ContentStreamWriter writer = new ContentStreamWriter(out); + writer.writeTokens(redactedTokens); + } + + formXObject.getCOSObject().removeItem(COSName.CONTENTS); + formXObject.getCOSObject().setItem(COSName.CONTENTS, newStream.getCOSObject()); + } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/RemoveCertSignController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/RemoveCertSignController.java index 553de109c..79a2ec955 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/RemoveCertSignController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/RemoveCertSignController.java @@ -7,11 +7,11 @@ import org.apache.pdfbox.pdmodel.PDDocumentCatalog; import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm; import org.apache.pdfbox.pdmodel.interactive.form.PDField; import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; -import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; @@ -21,6 +21,7 @@ import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.SecurityApi; import stirling.software.common.model.api.PDFFile; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.WebResponseUtils; @SecurityApi @@ -29,7 +30,7 @@ public class RemoveCertSignController { private final CustomPDFDocumentFactory pdfDocumentFactory; - @AutoJobPostMapping(consumes = "multipart/form-data", value = "/remove-cert-sign") + @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/remove-cert-sign") @StandardPdfResponse @Operation( summary = "Remove digital signature from PDF", @@ -62,7 +63,6 @@ public class RemoveCertSignController { // Return the modified PDF as a response return WebResponseUtils.pdfDocToWebResponse( document, - Filenames.toSimpleFileName(pdf.getOriginalFilename()).replaceFirst("[.][^.]+$", "") - + "_unsigned.pdf"); + GeneralUtils.generateFilename(pdf.getOriginalFilename(), "_unsigned.pdf")); } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/SanitizeController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/SanitizeController.java index 70a46a22e..a2cacce53 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/SanitizeController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/SanitizeController.java @@ -1,49 +1,56 @@ package stirling.software.SPDF.controller.api.security; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.util.List; import org.apache.pdfbox.cos.COSDictionary; import org.apache.pdfbox.cos.COSName; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocumentCatalog; import org.apache.pdfbox.pdmodel.PDDocumentInformation; +import org.apache.pdfbox.pdmodel.PDDocumentNameDictionary; import org.apache.pdfbox.pdmodel.PDPage; -import org.apache.pdfbox.pdmodel.PDPageTree; -import org.apache.pdfbox.pdmodel.PDResources; import org.apache.pdfbox.pdmodel.common.PDMetadata; import org.apache.pdfbox.pdmodel.interactive.action.PDAction; import org.apache.pdfbox.pdmodel.interactive.action.PDActionJavaScript; import org.apache.pdfbox.pdmodel.interactive.action.PDActionLaunch; import org.apache.pdfbox.pdmodel.interactive.action.PDActionURI; +import org.apache.pdfbox.pdmodel.interactive.action.PDDocumentCatalogAdditionalActions; import org.apache.pdfbox.pdmodel.interactive.action.PDFormFieldAdditionalActions; +import org.apache.pdfbox.pdmodel.interactive.action.PDPageAdditionalActions; import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationFileAttachment; import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationLink; import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget; import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm; import org.apache.pdfbox.pdmodel.interactive.form.PDField; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; -import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.config.swagger.StandardPdfResponse; import stirling.software.SPDF.model.api.security.SanitizePdfRequest; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.SecurityApi; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.WebResponseUtils; +@Slf4j @SecurityApi @RequiredArgsConstructor public class SanitizeController { private final CustomPDFDocumentFactory pdfDocumentFactory; - @AutoJobPostMapping(consumes = "multipart/form-data", value = "/sanitize-pdf") + @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/sanitize-pdf") @StandardPdfResponse @Operation( summary = "Sanitize a PDF file", @@ -85,14 +92,17 @@ public class SanitizeController { sanitizeFonts(document); } - return WebResponseUtils.pdfDocToWebResponse( - document, - Filenames.toSimpleFileName(inputFile.getOriginalFilename()) - .replaceFirst("[.][^.]+$", "") - + "_sanitized.pdf"); + // Save the sanitized document to output stream + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + document.save(outputStream); + document.close(); + + return WebResponseUtils.bytesToWebResponse( + outputStream.toByteArray(), + GeneralUtils.generateFilename(inputFile.getOriginalFilename(), "_sanitized.pdf")); } - private void sanitizeJavaScript(PDDocument document) throws IOException { + private static void sanitizeJavaScript(PDDocument document) throws IOException { // Get the root dictionary (catalog) of the PDF PDDocumentCatalog catalog = document.getDocumentCatalog(); @@ -111,7 +121,61 @@ public class SanitizeController { } } + if (catalog.getOpenAction() instanceof PDActionJavaScript) { + catalog.setOpenAction(null); + } + + PDDocumentCatalogAdditionalActions catalogActions = catalog.getActions(); + if (catalogActions != null) { + if (catalogActions.getWC() instanceof PDActionJavaScript) { + catalogActions.setWC(null); + } + if (catalogActions.getWS() instanceof PDActionJavaScript) { + catalogActions.setWS(null); + } + if (catalogActions.getDS() instanceof PDActionJavaScript) { + catalogActions.setDS(null); + } + if (catalogActions.getWP() instanceof PDActionJavaScript) { + catalogActions.setWP(null); + } + if (catalogActions.getDP() instanceof PDActionJavaScript) { + catalogActions.setDP(null); + } + } + + PDAcroForm acroForm = catalog.getAcroForm(); + if (acroForm != null) { + for (PDField field : acroForm.getFields()) { + PDFormFieldAdditionalActions actions = field.getActions(); + if (actions != null) { + if (actions.getC() instanceof PDActionJavaScript) { + actions.setC(null); + } + if (actions.getF() instanceof PDActionJavaScript) { + actions.setF(null); + } + if (actions.getK() instanceof PDActionJavaScript) { + actions.setK(null); + } + if (actions.getV() instanceof PDActionJavaScript) { + actions.setV(null); + } + } + } + } + for (PDPage page : document.getPages()) { + PDPageAdditionalActions pageActions = page.getActions(); + if (pageActions != null) { + if (pageActions.getO() instanceof PDActionJavaScript) { + pageActions.setO(null); + } + if (pageActions.getC() instanceof PDActionJavaScript) { + pageActions.setC(null); + } + } + for (PDAnnotation annotation : page.getAnnotations()) { if (annotation instanceof PDAnnotationWidget widget) { PDAction action = widget.getAction(); @@ -120,41 +184,26 @@ public class SanitizeController { } } } - PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm(); - if (acroForm != null) { - for (PDField field : acroForm.getFields()) { - PDFormFieldAdditionalActions actions = field.getActions(); - if (actions != null) { - if (actions.getC() instanceof PDActionJavaScript) { - actions.setC(null); - } - if (actions.getF() instanceof PDActionJavaScript) { - actions.setF(null); - } - if (actions.getK() instanceof PDActionJavaScript) { - actions.setK(null); - } - if (actions.getV() instanceof PDActionJavaScript) { - actions.setV(null); - } - } - } + } + } + + private static void sanitizeEmbeddedFiles(PDDocument document) throws IOException { + PDDocumentCatalog catalog = document.getDocumentCatalog(); + PDDocumentNameDictionary names = catalog.getNames(); + if (names != null) { + names.setEmbeddedFiles(null); + } + + for (PDPage page : document.getPages()) { + List annotations = page.getAnnotations(); + if (annotations != null && !annotations.isEmpty()) { + annotations.removeIf( + annotation -> annotation instanceof PDAnnotationFileAttachment); } } } - private void sanitizeEmbeddedFiles(PDDocument document) { - PDPageTree allPages = document.getPages(); - - for (PDPage page : allPages) { - PDResources res = page.getResources(); - if (res != null && res.getCOSObject() != null) { - res.getCOSObject().removeItem(COSName.getPDFName("EmbeddedFiles")); - } - } - } - - private void sanitizeXMPMetadata(PDDocument document) { + private static void sanitizeXMPMetadata(PDDocument document) { if (document.getDocumentCatalog() != null) { PDMetadata metadata = document.getDocumentCatalog().getMetadata(); if (metadata != null) { @@ -163,7 +212,7 @@ public class SanitizeController { } } - private void sanitizeDocumentInfoMetadata(PDDocument document) { + private static void sanitizeDocumentInfoMetadata(PDDocument document) { PDDocumentInformation docInfo = document.getDocumentInformation(); if (docInfo != null) { PDDocumentInformation newInfo = new PDDocumentInformation(); @@ -171,14 +220,12 @@ public class SanitizeController { } } - private void sanitizeLinks(PDDocument document) throws IOException { + private static void sanitizeLinks(PDDocument document) throws IOException { for (PDPage page : document.getPages()) { for (PDAnnotation annotation : page.getAnnotations()) { - if (annotation != null && annotation instanceof PDAnnotationLink linkAnnotation) { + if (annotation instanceof PDAnnotationLink linkAnnotation) { PDAction action = linkAnnotation.getAction(); - if (action != null - && (action instanceof PDActionLaunch - || action instanceof PDActionURI)) { + if ((action instanceof PDActionLaunch || action instanceof PDActionURI)) { linkAnnotation.setAction(null); } } @@ -186,7 +233,7 @@ public class SanitizeController { } } - private void sanitizeFonts(PDDocument document) { + private static void sanitizeFonts(PDDocument document) { for (PDPage page : document.getPages()) { if (page != null && page.getResources() != null diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/WatermarkController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/WatermarkController.java index 47fe17e93..3c475a354 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/WatermarkController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/WatermarkController.java @@ -24,13 +24,13 @@ import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; import org.apache.pdfbox.pdmodel.graphics.state.PDExtendedGraphicsState; import org.apache.pdfbox.util.Matrix; import org.springframework.core.io.ClassPathResource; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.InitBinder; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; -import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; @@ -40,7 +40,9 @@ import stirling.software.SPDF.model.api.security.AddWatermarkRequest; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.SecurityApi; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.PdfUtils; +import stirling.software.common.util.RegexPatternUtils; import stirling.software.common.util.WebResponseUtils; @SecurityApi @@ -61,7 +63,7 @@ public class WatermarkController { }); } - @AutoJobPostMapping(consumes = "multipart/form-data", value = "/add-watermark") + @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/add-watermark") @StandardPdfResponse @Operation( summary = "Add watermark to a PDF file", @@ -146,11 +148,10 @@ public class WatermarkController { document = convertedPdf; } + // Return the watermarked PDF as a response return WebResponseUtils.pdfDocToWebResponse( document, - Filenames.toSimpleFileName(pdfFile.getOriginalFilename()) - .replaceFirst("[.][^.]+$", "") - + "_watermarked.pdf"); + GeneralUtils.generateFilename(pdfFile.getOriginalFilename(), "_watermarked.pdf")); } private void addTextWatermark( @@ -167,39 +168,25 @@ public class WatermarkController { throws IOException { String resourceDir = ""; PDFont font = new PDType1Font(Standard14Fonts.FontName.HELVETICA); - switch (alphabet) { - case "arabic": - resourceDir = "static/fonts/NotoSansArabic-Regular.ttf"; - break; - case "japanese": - resourceDir = "static/fonts/Meiryo.ttf"; - break; - case "korean": - resourceDir = "static/fonts/malgun.ttf"; - break; - case "chinese": - resourceDir = "static/fonts/SimSun.ttf"; - break; - case "thai": - resourceDir = "static/fonts/NotoSansThai-Regular.ttf"; - break; - case "roman": - default: - resourceDir = "static/fonts/NotoSans-Regular.ttf"; - break; - } + resourceDir = + switch (alphabet) { + case "arabic" -> "static/fonts/NotoSansArabic-Regular.ttf"; + case "japanese" -> "static/fonts/Meiryo.ttf"; + case "korean" -> "static/fonts/malgun.ttf"; + case "chinese" -> "static/fonts/SimSun.ttf"; + case "thai" -> "static/fonts/NotoSansThai-Regular.ttf"; + default -> "static/fonts/NotoSans-Regular.ttf"; + }; - if (!"".equals(resourceDir)) { - ClassPathResource classPathResource = new ClassPathResource(resourceDir); - String fileExtension = resourceDir.substring(resourceDir.lastIndexOf(".")); - File tempFile = Files.createTempFile("NotoSansFont", fileExtension).toFile(); - try (InputStream is = classPathResource.getInputStream(); - FileOutputStream os = new FileOutputStream(tempFile)) { - IOUtils.copy(is, os); - font = PDType0Font.load(document, tempFile); - } finally { - if (tempFile != null) Files.deleteIfExists(tempFile.toPath()); - } + ClassPathResource classPathResource = new ClassPathResource(resourceDir); + String fileExtension = resourceDir.substring(resourceDir.lastIndexOf(".")); + File tempFile = Files.createTempFile("NotoSansFont", fileExtension).toFile(); + try (InputStream is = classPathResource.getInputStream(); + FileOutputStream os = new FileOutputStream(tempFile)) { + IOUtils.copy(is, os); + font = PDType0Font.load(document, tempFile); + } finally { + Files.deleteIfExists(tempFile.toPath()); } contentStream.setFont(font, fontSize); @@ -216,7 +203,8 @@ public class WatermarkController { } contentStream.setNonStrokingColor(redactColor); - String[] textLines = watermarkText.split("\\\\n"); + String[] textLines = + RegexPatternUtils.getInstance().getEscapedNewlinePattern().split(watermarkText); float maxLineWidth = 0; for (int i = 0; i < textLines.length; ++i) { diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/web/ConverterWebController.java b/app/core/src/main/java/stirling/software/SPDF/controller/web/ConverterWebController.java index bec23184c..c14d1ef56 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/web/ConverterWebController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/web/ConverterWebController.java @@ -1,11 +1,14 @@ package stirling.software.SPDF.controller.web; +import org.springframework.http.HttpStatus; import org.springframework.ui.Model; +import org.springframework.web.server.ResponseStatusException; import org.springframework.web.servlet.ModelAndView; import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.tags.Tag; +import stirling.software.SPDF.config.EndpointConfiguration; import stirling.software.common.model.ApplicationProperties; import stirling.software.common.util.ApplicationContextProvider; import stirling.software.common.util.CheckProgramInstall; @@ -22,6 +25,42 @@ public class ConverterWebController { return "convert/img-to-pdf"; } + @Deprecated + // @GetMapping("/cbz-to-pdf") + @Hidden + public String convertCbzToPdfForm(Model model) { + model.addAttribute("currentPage", "cbz-to-pdf"); + return "convert/cbz-to-pdf"; + } + + @Deprecated + // @GetMapping("/pdf-to-cbz") + @Hidden + public String convertPdfToCbzForm(Model model) { + model.addAttribute("currentPage", "pdf-to-cbz"); + return "convert/pdf-to-cbz"; + } + + @Deprecated + // @GetMapping("/cbr-to-pdf") + @Hidden + public String convertCbrToPdfForm(Model model) { + model.addAttribute("currentPage", "cbr-to-pdf"); + return "convert/cbr-to-pdf"; + } + + @Deprecated + // @GetMapping("/pdf-to-cbr") + @Hidden + public String convertPdfToCbrForm(Model model) { + if (!ApplicationContextProvider.getBean(EndpointConfiguration.class) + .isEndpointEnabled("pdf-to-cbr")) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND); + } + model.addAttribute("currentPage", "pdf-to-cbr"); + return "convert/pdf-to-cbr"; + } + @Deprecated // @GetMapping("/html-to-pdf") @Hidden diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java b/app/core/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java index b8e6036f9..d2a2e5a17 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java @@ -20,6 +20,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.Getter; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.model.SignatureFile; @@ -74,7 +76,7 @@ public class GeneralWebController { new ObjectMapper() .readValue(config, new TypeReference>() {}); String name = (String) jsonContent.get("name"); - if (name == null || name.length() < 1) { + if (name == null || name.isEmpty()) { String filename = jsonFiles .get(pipelineConfigs.indexOf(config)) @@ -91,7 +93,7 @@ public class GeneralWebController { log.error("exception", e); } } - if (pipelineConfigsWithNames.size() == 0) { + if (pipelineConfigsWithNames.isEmpty()) { Map configWithName = new HashMap<>(); configWithName.put("json", ""); configWithName.put("name", "No preloaded configs found"); @@ -295,21 +297,16 @@ public class GeneralWebController { } public String getFormatFromExtension(String extension) { - switch (extension) { - case "ttf": - return "truetype"; - case "woff": - return "woff"; - case "woff2": - return "woff2"; - case "eot": - return "embedded-opentype"; - case "svg": - return "svg"; - default: - // or throw an exception if an unexpected extension is encountered - return ""; - } + return switch (extension) { + case "ttf" -> "truetype"; + case "woff" -> "woff"; + case "woff2" -> "woff2"; + case "eot" -> "embedded-opentype"; + case "svg" -> "svg"; + default -> + // or throw an exception if an unexpected extension is encountered + ""; + }; } @Deprecated @@ -336,6 +333,8 @@ public class GeneralWebController { return "remove-image-pdf"; } + @Setter + @Getter public class FontResource { private String name; @@ -349,29 +348,5 @@ public class GeneralWebController { this.extension = extension; this.type = getFormatFromExtension(extension); } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getExtension() { - return extension; - } - - public void setExtension(String extension) { - this.extension = extension; - } - - public String getType() { - return type; - } - - public void setType(String type) { - this.type = type; - } } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/web/MetricsController.java b/app/core/src/main/java/stirling/software/SPDF/controller/web/MetricsController.java index 5049d55b8..8f17e0baf 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/web/MetricsController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/web/MetricsController.java @@ -16,7 +16,9 @@ import io.swagger.v3.oas.annotations.Parameter; import jakarta.annotation.PostConstruct; +import lombok.Getter; import lombok.RequiredArgsConstructor; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.config.EndpointInspector; @@ -358,6 +360,8 @@ public class MetricsController { return String.format("%dd %dh %dm %ds", days, hours, minutes, seconds); } + @Setter + @Getter public static class EndpointCount { private String endpoint; @@ -368,21 +372,5 @@ public class MetricsController { this.endpoint = endpoint; this.count = count; } - - public String getEndpoint() { - return endpoint; - } - - public void setEndpoint(String endpoint) { - this.endpoint = endpoint; - } - - public double getCount() { - return count; - } - - public void setCount(double count) { - this.count = count; - } } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/web/UploadLimitService.java b/app/core/src/main/java/stirling/software/SPDF/controller/web/UploadLimitService.java index 2c4ed9bec..597057e90 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/web/UploadLimitService.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/web/UploadLimitService.java @@ -1,7 +1,6 @@ package stirling.software.SPDF.controller.web; import java.util.Locale; -import java.util.regex.Pattern; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -17,31 +16,44 @@ public class UploadLimitService { @Autowired private ApplicationProperties applicationProperties; public long getUploadLimit() { - String maxUploadSize = + String raw = applicationProperties.getSystem().getFileUploadLimit() != null ? applicationProperties.getSystem().getFileUploadLimit() : ""; - - if (maxUploadSize.isEmpty()) { - return 0; - } else if (!Pattern.compile("^[1-9][0-9]{0,2}[KMGkmg][Bb]$") - .matcher(maxUploadSize) - .matches()) { - log.error( - "Invalid maxUploadSize format. Expected format: [1-9][0-9]{0,2}[KMGkmg][Bb], but got: {}", - maxUploadSize); - return 0; - } else { - String unit = maxUploadSize.replaceAll("[1-9][0-9]{0,2}", "").toUpperCase(); - String number = maxUploadSize.replaceAll("[KMGkmg][Bb]", ""); - long size = Long.parseLong(number); - return switch (unit) { - case "KB" -> size * 1024; - case "MB" -> size * 1024 * 1024; - case "GB" -> size * 1024 * 1024 * 1024; - default -> 0; - }; + if (raw == null || raw.isEmpty()) { + return 0L; } + String s = raw.trim(); + // Normalize case for unit parsing + String upper = s.toUpperCase(Locale.ROOT); + // Expect strictly: 0-999 followed by KB/MB/GB + // Find last two chars as unit if length >= 3 + if (upper.length() < 3) return 0L; + String unit = upper.substring(upper.length() - 2); + if (!unit.equals("KB") && !unit.equals("MB") && !unit.equals("GB")) { + return 0L; + } + String numPart = upper.substring(0, upper.length() - 2); + // Disallow signs, decimals, spaces; only 1-3 digits (allow 0) + if (numPart.length() > 3) { + return 0L; + } + for (int i = 0; i < numPart.length(); i++) { + char c = numPart.charAt(i); + if (c < '0' || c > '9') return 0L; + } + long value; + try { + value = Long.parseLong(numPart); + } catch (NumberFormatException e) { + return 0L; + } + return switch (unit) { + case "KB" -> value * 1024L; + case "MB" -> value * 1024L * 1024L; + case "GB" -> value * 1024L * 1024L * 1024L; + default -> 0L; + }; } // TODO: why do this server side not client? diff --git a/app/core/src/main/java/stirling/software/SPDF/model/ApiEndpoint.java b/app/core/src/main/java/stirling/software/SPDF/model/ApiEndpoint.java index dfb06f0d8..ba4a62eeb 100644 --- a/app/core/src/main/java/stirling/software/SPDF/model/ApiEndpoint.java +++ b/app/core/src/main/java/stirling/software/SPDF/model/ApiEndpoint.java @@ -5,10 +5,12 @@ import java.util.Map; import com.fasterxml.jackson.databind.JsonNode; +import lombok.Getter; + public class ApiEndpoint { private final String name; private Map parameters; - private final String description; + @Getter private final String description; public ApiEndpoint(String name, JsonNode postNode) { this.name = name; @@ -31,10 +33,6 @@ public class ApiEndpoint { return true; } - public String getDescription() { - return description; - } - @Override public String toString() { return "ApiEndpoint [name=" + name + ", parameters=" + parameters + "]"; diff --git a/app/core/src/main/java/stirling/software/SPDF/model/SignatureFile.java b/app/core/src/main/java/stirling/software/SPDF/model/SignatureFile.java index 7d82ebf0f..89ea49644 100644 --- a/app/core/src/main/java/stirling/software/SPDF/model/SignatureFile.java +++ b/app/core/src/main/java/stirling/software/SPDF/model/SignatureFile.java @@ -2,8 +2,10 @@ package stirling.software.SPDF.model; import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; @Data +@NoArgsConstructor @AllArgsConstructor public class SignatureFile { private String fileName; diff --git a/app/core/src/main/java/stirling/software/SPDF/model/api/converters/ConvertCbrToPdfRequest.java b/app/core/src/main/java/stirling/software/SPDF/model/api/converters/ConvertCbrToPdfRequest.java new file mode 100644 index 000000000..4e4b64ef4 --- /dev/null +++ b/app/core/src/main/java/stirling/software/SPDF/model/api/converters/ConvertCbrToPdfRequest.java @@ -0,0 +1,23 @@ +package stirling.software.SPDF.model.api.converters; + +import org.springframework.web.multipart.MultipartFile; + +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode +public class ConvertCbrToPdfRequest { + + @Schema( + description = "The input CBR file to be converted to a PDF file", + requiredMode = Schema.RequiredMode.REQUIRED) + private MultipartFile fileInput; + + @Schema( + description = "Optimize the output PDF for ebook reading using Ghostscript", + defaultValue = "false") + private boolean optimizeForEbook; +} diff --git a/app/core/src/main/java/stirling/software/SPDF/model/api/converters/ConvertCbzToPdfRequest.java b/app/core/src/main/java/stirling/software/SPDF/model/api/converters/ConvertCbzToPdfRequest.java new file mode 100644 index 000000000..08123c1e4 --- /dev/null +++ b/app/core/src/main/java/stirling/software/SPDF/model/api/converters/ConvertCbzToPdfRequest.java @@ -0,0 +1,23 @@ +package stirling.software.SPDF.model.api.converters; + +import org.springframework.web.multipart.MultipartFile; + +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode +public class ConvertCbzToPdfRequest { + + @Schema( + description = "The input CBZ file to be converted to a PDF file", + requiredMode = Schema.RequiredMode.REQUIRED) + private MultipartFile fileInput; + + @Schema( + description = "Optimize the output PDF for ebook reading using Ghostscript", + defaultValue = "false") + private boolean optimizeForEbook; +} diff --git a/app/core/src/main/java/stirling/software/SPDF/model/api/converters/ConvertPDFToMarkdown.java b/app/core/src/main/java/stirling/software/SPDF/model/api/converters/ConvertPDFToMarkdown.java index 5febf16d0..5b820e607 100644 --- a/app/core/src/main/java/stirling/software/SPDF/model/api/converters/ConvertPDFToMarkdown.java +++ b/app/core/src/main/java/stirling/software/SPDF/model/api/converters/ConvertPDFToMarkdown.java @@ -1,25 +1,28 @@ package stirling.software.SPDF.model.api.converters; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; + +import lombok.RequiredArgsConstructor; import stirling.software.SPDF.config.swagger.MarkdownConversionResponse; import stirling.software.common.annotations.AutoJobPostMapping; +import stirling.software.common.annotations.api.ConvertApi; import stirling.software.common.model.api.PDFFile; import stirling.software.common.util.PDFToFile; +import stirling.software.common.util.TempFileManager; -@RestController -@Tag(name = "Convert", description = "Convert APIs") -@RequestMapping("/api/v1/convert") +@ConvertApi +@RequiredArgsConstructor public class ConvertPDFToMarkdown { - @AutoJobPostMapping(consumes = "multipart/form-data", value = "/pdf/markdown") + private final TempFileManager tempFileManager; + + @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/markdown") @MarkdownConversionResponse @Operation( summary = "Convert PDF to Markdown", @@ -28,7 +31,7 @@ public class ConvertPDFToMarkdown { public ResponseEntity processPdfToMarkdown(@ModelAttribute PDFFile file) throws Exception { MultipartFile inputFile = file.getFileInput(); - PDFToFile pdfToFile = new PDFToFile(); + PDFToFile pdfToFile = new PDFToFile(tempFileManager); return pdfToFile.processPdfToMarkdown(inputFile); } } diff --git a/app/core/src/main/java/stirling/software/SPDF/model/api/converters/ConvertPdfToCbrRequest.java b/app/core/src/main/java/stirling/software/SPDF/model/api/converters/ConvertPdfToCbrRequest.java new file mode 100644 index 000000000..9f79472dc --- /dev/null +++ b/app/core/src/main/java/stirling/software/SPDF/model/api/converters/ConvertPdfToCbrRequest.java @@ -0,0 +1,24 @@ +package stirling.software.SPDF.model.api.converters; + +import org.springframework.web.multipart.MultipartFile; + +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode +public class ConvertPdfToCbrRequest { + + @Schema( + description = "The input PDF file to be converted to a CBR file", + requiredMode = Schema.RequiredMode.REQUIRED) + private MultipartFile fileInput; + + @Schema( + description = "The DPI (Dots Per Inch) for rendering PDF pages as images", + example = "150", + requiredMode = Schema.RequiredMode.REQUIRED) + private int dpi = 150; +} diff --git a/app/core/src/main/java/stirling/software/SPDF/model/api/converters/ConvertPdfToCbzRequest.java b/app/core/src/main/java/stirling/software/SPDF/model/api/converters/ConvertPdfToCbzRequest.java new file mode 100644 index 000000000..2cc216d4c --- /dev/null +++ b/app/core/src/main/java/stirling/software/SPDF/model/api/converters/ConvertPdfToCbzRequest.java @@ -0,0 +1,24 @@ +package stirling.software.SPDF.model.api.converters; + +import org.springframework.web.multipart.MultipartFile; + +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode +public class ConvertPdfToCbzRequest { + + @Schema( + description = "The input PDF file to be converted to a CBZ file", + requiredMode = Schema.RequiredMode.REQUIRED) + private MultipartFile fileInput; + + @Schema( + description = "The DPI (Dots Per Inch) for rendering PDF pages as images", + example = "150", + requiredMode = Schema.RequiredMode.REQUIRED) + private int dpi = 150; +} diff --git a/app/core/src/main/java/stirling/software/SPDF/model/api/converters/ConvertToImageRequest.java b/app/core/src/main/java/stirling/software/SPDF/model/api/converters/ConvertToImageRequest.java index 149676946..dbbd9f0eb 100644 --- a/app/core/src/main/java/stirling/software/SPDF/model/api/converters/ConvertToImageRequest.java +++ b/app/core/src/main/java/stirling/software/SPDF/model/api/converters/ConvertToImageRequest.java @@ -39,4 +39,9 @@ public class ConvertToImageRequest extends PDFWithPageNums { defaultValue = "300", requiredMode = Schema.RequiredMode.REQUIRED) private Integer dpi; + + @Schema( + description = "Include annotations such as comments in the output image(s)", + defaultValue = "false") + private Boolean includeAnnotations; } diff --git a/app/core/src/main/java/stirling/software/SPDF/model/api/general/CropPdfForm.java b/app/core/src/main/java/stirling/software/SPDF/model/api/general/CropPdfForm.java index 913f94a10..480169468 100644 --- a/app/core/src/main/java/stirling/software/SPDF/model/api/general/CropPdfForm.java +++ b/app/core/src/main/java/stirling/software/SPDF/model/api/general/CropPdfForm.java @@ -26,4 +26,9 @@ public class CropPdfForm extends PDFFile { @Schema(description = "The height of the crop area", type = "number") private float height; + + @Schema( + description = "Whether to remove text outside the crop area (keeps images)", + type = "boolean") + private boolean removeDataOutsideCrop = true; } diff --git a/app/core/src/main/java/stirling/software/SPDF/model/api/misc/AddPageNumbersRequest.java b/app/core/src/main/java/stirling/software/SPDF/model/api/misc/AddPageNumbersRequest.java index bbf3602d1..3e116742d 100644 --- a/app/core/src/main/java/stirling/software/SPDF/model/api/misc/AddPageNumbersRequest.java +++ b/app/core/src/main/java/stirling/software/SPDF/model/api/misc/AddPageNumbersRequest.java @@ -32,6 +32,13 @@ public class AddPageNumbersRequest extends PDFWithPageNums { requiredMode = RequiredMode.REQUIRED) private String fontType; + @Schema( + description = "Hex colour for page numbers (e.g. #FF0000)", + example = "#000000", + defaultValue = "#000000", + requiredMode = RequiredMode.NOT_REQUIRED) + private String fontColor; + @Schema( description = "Position: 1-9 representing positions on the page (1=top-left, 2=top-center," diff --git a/app/core/src/main/java/stirling/software/SPDF/model/api/misc/ReplaceAndInvertColorRequest.java b/app/core/src/main/java/stirling/software/SPDF/model/api/misc/ReplaceAndInvertColorRequest.java index 50ef14b1e..02bbcd6ad 100644 --- a/app/core/src/main/java/stirling/software/SPDF/model/api/misc/ReplaceAndInvertColorRequest.java +++ b/app/core/src/main/java/stirling/software/SPDF/model/api/misc/ReplaceAndInvertColorRequest.java @@ -17,7 +17,12 @@ public class ReplaceAndInvertColorRequest extends PDFFile { description = "Replace and Invert color options of a pdf.", requiredMode = Schema.RequiredMode.REQUIRED, defaultValue = "HIGH_CONTRAST_COLOR", - allowableValues = {"HIGH_CONTRAST_COLOR", "CUSTOM_COLOR", "FULL_INVERSION"}) + allowableValues = { + "HIGH_CONTRAST_COLOR", + "CUSTOM_COLOR", + "FULL_INVERSION", + "COLOR_SPACE_CONVERSION" + }) private ReplaceAndInvert replaceAndInvertOption; @Schema( diff --git a/app/core/src/main/java/stirling/software/SPDF/model/api/misc/ScannerEffectRequest.java b/app/core/src/main/java/stirling/software/SPDF/model/api/misc/ScannerEffectRequest.java index 72ecb42f6..411f6dbe6 100644 --- a/app/core/src/main/java/stirling/software/SPDF/model/api/misc/ScannerEffectRequest.java +++ b/app/core/src/main/java/stirling/software/SPDF/model/api/misc/ScannerEffectRequest.java @@ -79,10 +79,6 @@ public class ScannerEffectRequest { @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; @@ -105,7 +101,7 @@ public class ScannerEffectRequest { this.noise = 1.0f; this.brightness = 1.02f; this.contrast = 1.05f; - this.resolution = 600; + this.resolution = 300; } public void applyMediumQualityPreset() { diff --git a/app/core/src/main/java/stirling/software/SPDF/model/api/security/SignPDFWithCertRequest.java b/app/core/src/main/java/stirling/software/SPDF/model/api/security/SignPDFWithCertRequest.java index d75f751f1..9b063d19f 100644 --- a/app/core/src/main/java/stirling/software/SPDF/model/api/security/SignPDFWithCertRequest.java +++ b/app/core/src/main/java/stirling/software/SPDF/model/api/security/SignPDFWithCertRequest.java @@ -15,20 +15,25 @@ public class SignPDFWithCertRequest extends PDFFile { @Schema( description = "The type of the digital certificate", - allowableValues = {"PEM", "PKCS12", "JKS", "SERVER"}, + allowableValues = {"PEM", "PKCS12", "PFX", "JKS", "SERVER"}, requiredMode = Schema.RequiredMode.REQUIRED) private String certType; @Schema( description = "The private key for the digital certificate (required for PEM type" - + " certificates)") + + " certificates, supports .pem, .der, or .key files)") private MultipartFile privateKeyFile; - @Schema(description = "The digital certificate (required for PEM type certificates)") + @Schema( + description = + "The digital certificate (required for PEM type certificates, supports" + + " .pem, .der, .crt, or .cer files)") private MultipartFile certFile; - @Schema(description = "The PKCS12 keystore file (required for PKCS12 type certificates)") + @Schema( + description = + "The PKCS12/PFX keystore file (required for PKCS12 or PFX type certificates)") private MultipartFile p12File; @Schema(description = "The JKS keystore file (Java Key Store)") diff --git a/app/core/src/main/java/stirling/software/SPDF/pdf/TextFinder.java b/app/core/src/main/java/stirling/software/SPDF/pdf/TextFinder.java index 4119b3eac..5ab1e725e 100644 --- a/app/core/src/main/java/stirling/software/SPDF/pdf/TextFinder.java +++ b/app/core/src/main/java/stirling/software/SPDF/pdf/TextFinder.java @@ -6,102 +6,213 @@ import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.text.PDFTextStripper; import org.apache.pdfbox.text.TextPosition; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.model.PDFText; +import stirling.software.common.util.RegexPatternUtils; @Slf4j public class TextFinder extends PDFTextStripper { - private final String searchText; + private final String searchTerm; private final boolean useRegex; private final boolean wholeWordSearch; - private final List textOccurrences = new ArrayList<>(); + @Getter private final List foundTexts = new ArrayList<>(); - public TextFinder(String searchText, boolean useRegex, boolean wholeWordSearch) + private final List pageTextPositions = new ArrayList<>(); + private final StringBuilder pageTextBuilder = new StringBuilder(); + + public TextFinder(String searchTerm, boolean useRegex, boolean wholeWordSearch) throws IOException { - this.searchText = searchText.toLowerCase(); + this.searchTerm = searchTerm; this.useRegex = useRegex; this.wholeWordSearch = wholeWordSearch; - setSortByPosition(true); + this.setWordSeparator(" "); } - private List findOccurrencesInText(String searchText, String content) { - List matches = new ArrayList<>(); - - Pattern pattern; - - if (useRegex) { - // Use regex-based search - pattern = - wholeWordSearch - ? Pattern.compile("\\b" + searchText + "\\b") - : Pattern.compile(searchText); - } else { - // Use normal text search - pattern = - wholeWordSearch - ? Pattern.compile("\\b" + Pattern.quote(searchText) + "\\b") - : Pattern.compile(Pattern.quote(searchText)); - } - - Matcher matcher = pattern.matcher(content); - while (matcher.find()) { - matches.add(new MatchInfo(matcher.start(), matcher.end() - matcher.start())); - } - return matches; + @Override + protected void startPage(PDPage page) throws IOException { + super.startPage(page); + pageTextPositions.clear(); + pageTextBuilder.setLength(0); } @Override protected void writeString(String text, List textPositions) { - for (MatchInfo match : findOccurrencesInText(searchText, text.toLowerCase())) { - int index = match.startIndex; - if (index + match.matchLength <= textPositions.size()) { - // Initial values based on the first character - TextPosition first = textPositions.get(index); - float minX = first.getX(); - float minY = first.getY(); - float maxX = first.getX() + first.getWidth(); - float maxY = first.getY() + first.getHeight(); + pageTextBuilder.append(text); + pageTextPositions.addAll(textPositions); + } - // Loop over the rest of the characters and adjust bounding box values - for (int i = index; i < index + match.matchLength; i++) { - TextPosition position = textPositions.get(i); - minX = Math.min(minX, position.getX()); - minY = Math.min(minY, position.getY()); - maxX = Math.max(maxX, position.getX() + position.getWidth()); - maxY = Math.max(maxY, position.getY() + position.getHeight()); - } + @Override + protected void writeWordSeparator() { + pageTextBuilder.append(getWordSeparator()); + pageTextPositions.add(null); // Placeholder for separator + } - textOccurrences.add( - new PDFText(getCurrentPageNo() - 1, minX, minY, maxX, maxY, text)); + @Override + protected void writeLineSeparator() { + pageTextBuilder.append(getLineSeparator()); + pageTextPositions.add(null); // Placeholder for separator + } + + @Override + protected void endPage(PDPage page) throws IOException { + String text = pageTextBuilder.toString(); + if (text.isEmpty() || this.searchTerm == null || this.searchTerm.isEmpty()) { + super.endPage(page); + return; + } + + String processedSearchTerm = this.searchTerm.trim(); + if (processedSearchTerm.isEmpty()) { + super.endPage(page); + return; + } + String regex = this.useRegex ? processedSearchTerm : "\\Q" + processedSearchTerm + "\\E"; + if (this.wholeWordSearch) { + if (processedSearchTerm.length() == 1 + && Character.isDigit(processedSearchTerm.charAt(0))) { + regex = "(? getTextLocations(PDDocument document) throws Exception { - this.getText(document); + // Use cached pattern compilation for better performance + Pattern pattern = RegexPatternUtils.getInstance().createSearchPattern(regex, true); + Matcher matcher = pattern.matcher(text); + log.debug( - "Found " - + textOccurrences.size() - + " occurrences of '" - + searchText - + "' in the document."); + "Searching for '{}' in page {} with regex '{}' (wholeWord: {}, useRegex: {})", + processedSearchTerm, + getCurrentPageNo(), + regex, + wholeWordSearch, + useRegex); - return textOccurrences; + int matchCount = 0; + while (matcher.find()) { + matchCount++; + int matchStart = matcher.start(); + int matchEnd = matcher.end(); + + log.debug( + "Found match #{} at positions {}-{}: '{}'", + matchCount, + matchStart, + matchEnd, + matcher.group()); + + float minX = Float.MAX_VALUE; + float minY = Float.MAX_VALUE; + float maxX = Float.MIN_VALUE; + float maxY = Float.MIN_VALUE; + boolean foundPosition = false; + + for (int i = matchStart; i < matchEnd; i++) { + if (i >= pageTextPositions.size()) { + log.debug( + "Position index {} exceeds available positions ({})", + i, + pageTextPositions.size()); + continue; + } + TextPosition pos = pageTextPositions.get(i); + if (pos != null) { + foundPosition = true; + minX = Math.min(minX, pos.getX()); + maxX = Math.max(maxX, pos.getX() + pos.getWidth()); + minY = Math.min(minY, pos.getY() - pos.getHeight()); + maxY = Math.max(maxY, pos.getY()); + } + } + + if (!foundPosition && matchStart < pageTextPositions.size()) { + log.debug( + "Attempting to find nearby positions for match at {}-{}", + matchStart, + matchEnd); + + for (int i = Math.max(0, matchStart - 5); + i < Math.min(pageTextPositions.size(), matchEnd + 5); + i++) { + TextPosition pos = pageTextPositions.get(i); + if (pos != null) { + foundPosition = true; + minX = Math.min(minX, pos.getX()); + maxX = Math.max(maxX, pos.getX() + pos.getWidth()); + minY = Math.min(minY, pos.getY() - pos.getHeight()); + maxY = Math.max(maxY, pos.getY()); + break; + } + } + } + + if (foundPosition) { + foundTexts.add( + new PDFText( + this.getCurrentPageNo() - 1, + minX, + minY, + maxX, + maxY, + matcher.group())); + log.debug( + "Added PDFText for match: page={}, bounds=({},{},{},{}), text='{}'", + getCurrentPageNo() - 1, + minX, + minY, + maxX, + maxY, + matcher.group()); + } else { + log.warn( + "Found text match '{}' but no valid position data at {}-{}", + matcher.group(), + matchStart, + matchEnd); + } + } + + log.debug( + "Page {} search complete: found {} matches for '{}'", + getCurrentPageNo(), + matchCount, + processedSearchTerm); + + super.endPage(page); } - private class MatchInfo { - int startIndex; - int matchLength; + public String getDebugInfo() { + StringBuilder debug = new StringBuilder(); + debug.append("Extracted text length: ").append(pageTextBuilder.length()).append("\n"); + debug.append("Position count: ").append(pageTextPositions.size()).append("\n"); + debug.append("Text content: '") + .append(pageTextBuilder.toString().replace("\n", "\\n").replace("\r", "\\r")) + .append("'\n"); - MatchInfo(int startIndex, int matchLength) { - this.startIndex = startIndex; - this.matchLength = matchLength; + String text = pageTextBuilder.toString(); + for (int i = 0; i < Math.min(text.length(), 50); i++) { + char c = text.charAt(i); + TextPosition pos = i < pageTextPositions.size() ? pageTextPositions.get(i) : null; + debug.append( + String.format( + " [%d] '%c' (0x%02X) -> %s\n", + i, + c, + (int) c, + pos != null + ? String.format("(%.1f,%.1f)", pos.getX(), pos.getY()) + : "null")); } + + return debug.toString(); } } diff --git a/app/core/src/main/java/stirling/software/SPDF/service/ApiDocService.java b/app/core/src/main/java/stirling/software/SPDF/service/ApiDocService.java index 0e46af08d..a51704fdd 100644 --- a/app/core/src/main/java/stirling/software/SPDF/service/ApiDocService.java +++ b/app/core/src/main/java/stirling/software/SPDF/service/ApiDocService.java @@ -5,7 +5,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.regex.Matcher; -import java.util.regex.Pattern; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpEntity; @@ -26,6 +25,7 @@ import stirling.software.SPDF.SPDFApplication; import stirling.software.SPDF.model.ApiEndpoint; import stirling.software.common.model.enumeration.Role; import stirling.software.common.service.UserServiceInterface; +import stirling.software.common.util.RegexPatternUtils; @Service @Slf4j @@ -52,8 +52,8 @@ public class ApiDocService { } public List getExtensionTypes(boolean output, String operationName) { - if (outputToFileTypes.size() == 0) { - outputToFileTypes.put("PDF", Arrays.asList("pdf")); + if (outputToFileTypes.isEmpty()) { + outputToFileTypes.put("PDF", List.of("pdf")); outputToFileTypes.put( "IMAGE", Arrays.asList( @@ -63,10 +63,10 @@ public class ApiDocService { "ZIP", Arrays.asList("zip", "rar", "7z", "tar", "gz", "bz2", "xz", "lz", "lzma", "z")); outputToFileTypes.put("WORD", Arrays.asList("doc", "docx", "odt", "rtf")); - outputToFileTypes.put("CSV", Arrays.asList("csv")); + outputToFileTypes.put("CSV", List.of("csv")); outputToFileTypes.put("JS", Arrays.asList("js", "jsx")); outputToFileTypes.put("HTML", Arrays.asList("html", "htm", "xhtml")); - outputToFileTypes.put("JSON", Arrays.asList("json")); + outputToFileTypes.put("JSON", List.of("json")); outputToFileTypes.put("TXT", Arrays.asList("txt", "text", "md", "markdown")); outputToFileTypes.put("PPT", Arrays.asList("ppt", "pptx", "odp")); outputToFileTypes.put("XML", Arrays.asList("xml", "xsd", "xsl")); @@ -74,7 +74,7 @@ public class ApiDocService { "BOOK", Arrays.asList("epub", "mobi", "azw3", "fb2", "txt", "docx")); // type. } - if (apiDocsJsonRootNode == null || apiDocumentation.size() == 0) { + if (apiDocsJsonRootNode == null || apiDocumentation.isEmpty()) { loadApiDocumentation(); } if (!apiDocumentation.containsKey(operationName)) { @@ -82,13 +82,11 @@ public class ApiDocService { } ApiEndpoint endpoint = apiDocumentation.get(operationName); String description = endpoint.getDescription(); - Pattern pattern = null; - if (output) { - pattern = Pattern.compile("Output:(\\w+)"); - } else { - pattern = Pattern.compile("Input:(\\w+)"); - } - Matcher matcher = pattern.matcher(description); + Matcher matcher = + (output + ? RegexPatternUtils.getInstance().getApiDocOutputTypePattern() + : RegexPatternUtils.getInstance().getApiDocInputTypePattern()) + .matcher(description); while (matcher.find()) { String type = matcher.group(1).toUpperCase(); if (outputToFileTypes.containsKey(type)) { @@ -138,7 +136,7 @@ public class ApiDocService { } public boolean isValidOperation(String operationName, Map parameters) { - if (apiDocumentation.size() == 0) { + if (apiDocumentation.isEmpty()) { loadApiDocumentation(); } if (!apiDocumentation.containsKey(operationName)) { @@ -149,7 +147,7 @@ public class ApiDocService { } public boolean isMultiInput(String operationName) { - if (apiDocsJsonRootNode == null || apiDocumentation.size() == 0) { + if (apiDocsJsonRootNode == null || apiDocumentation.isEmpty()) { loadApiDocumentation(); } if (!apiDocumentation.containsKey(operationName)) { @@ -157,8 +155,8 @@ public class ApiDocService { } ApiEndpoint endpoint = apiDocumentation.get(operationName); String description = endpoint.getDescription(); - Pattern pattern = Pattern.compile("Type:(\\w+)"); - Matcher matcher = pattern.matcher(description); + Matcher matcher = + RegexPatternUtils.getInstance().getApiDocTypePattern().matcher(description); if (matcher.find()) { String type = matcher.group(1); return type.startsWith("MI"); diff --git a/app/core/src/main/java/stirling/software/SPDF/service/AttachmentService.java b/app/core/src/main/java/stirling/software/SPDF/service/AttachmentService.java index 4aa6dfe41..6eb74dfb8 100644 --- a/app/core/src/main/java/stirling/software/SPDF/service/AttachmentService.java +++ b/app/core/src/main/java/stirling/software/SPDF/service/AttachmentService.java @@ -3,6 +3,9 @@ package stirling.software.SPDF.service; import static stirling.software.common.util.AttachmentUtils.setCatalogViewerPreferences; import java.io.IOException; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.List; @@ -54,8 +57,13 @@ public class AttachmentService implements AttachmentServiceInterface { PDEmbeddedFile embeddedFile = new PDEmbeddedFile(document, attachment.getInputStream()); embeddedFile.setSize((int) attachment.getSize()); - embeddedFile.setCreationDate(new GregorianCalendar()); - embeddedFile.setModDate(new GregorianCalendar()); + // use java.time.Instant and convert to GregorianCalendar for PDFBox + Instant now = Instant.now(); + GregorianCalendar nowCal = + GregorianCalendar.from( + ZonedDateTime.ofInstant(now, ZoneId.systemDefault())); + embeddedFile.setCreationDate(nowCal); + embeddedFile.setModDate(nowCal); String contentType = attachment.getContentType(); if (StringUtils.isNotBlank(contentType)) { embeddedFile.setSubtype(contentType); diff --git a/app/core/src/main/java/stirling/software/SPDF/service/MetricsAggregatorService.java b/app/core/src/main/java/stirling/software/SPDF/service/MetricsAggregatorService.java index 181757a04..37ec6dd76 100644 --- a/app/core/src/main/java/stirling/software/SPDF/service/MetricsAggregatorService.java +++ b/app/core/src/main/java/stirling/software/SPDF/service/MetricsAggregatorService.java @@ -4,8 +4,6 @@ import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; @@ -13,15 +11,15 @@ import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.search.Search; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.config.EndpointInspector; import stirling.software.common.service.PostHogService; @Service @RequiredArgsConstructor +@Slf4j public class MetricsAggregatorService { - private static final Logger logger = LoggerFactory.getLogger(MetricsAggregatorService.class); - private final MeterRegistry meterRegistry; private final PostHogService postHogService; private final EndpointInspector endpointInspector; @@ -31,7 +29,7 @@ public class MetricsAggregatorService { public void aggregateAndSendMetrics() { Map metrics = new HashMap<>(); - final boolean validateGetEndpoints = endpointInspector.getValidGetEndpoints().size() != 0; + final boolean validateGetEndpoints = !endpointInspector.getValidGetEndpoints().isEmpty(); Search.in(meterRegistry) .name("http.requests") .counters() @@ -66,7 +64,7 @@ public class MetricsAggregatorService { if ("GET".equals(method) && validateGetEndpoints && !endpointInspector.isValidGetEndpoint(uri)) { - logger.debug("Skipping invalid GET endpoint: {}", uri); + log.debug("Skipping invalid GET endpoint: {}", uri); return; } @@ -77,7 +75,7 @@ public class MetricsAggregatorService { double lastCount = lastSentMetrics.getOrDefault(key, 0.0); double difference = currentCount - lastCount; if (difference > 0) { - logger.debug("{}, {}", key, difference); + log.debug("{}, {}", key, difference); metrics.put(key, difference); lastSentMetrics.put(key, currentCount); } diff --git a/app/core/src/main/java/stirling/software/SPDF/service/SignatureService.java b/app/core/src/main/java/stirling/software/SPDF/service/SignatureService.java index 16385392e..579ea5507 100644 --- a/app/core/src/main/java/stirling/software/SPDF/service/SignatureService.java +++ b/app/core/src/main/java/stirling/software/SPDF/service/SignatureService.java @@ -7,6 +7,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; +import java.util.stream.Stream; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; @@ -66,10 +67,11 @@ public class SignatureService { private List getSignaturesFromFolder(Path folder, String category) throws IOException { - return Files.list(folder) - .filter(path -> isImageFile(path)) - .map(path -> new SignatureFile(path.getFileName().toString(), category)) - .toList(); + try (Stream stream = Files.list(folder)) { + return stream.filter(this::isImageFile) + .map(path -> new SignatureFile(path.getFileName().toString(), category)) + .toList(); + } } public byte[] getSignatureBytes(String username, String fileName) throws IOException { diff --git a/app/core/src/main/java/stirling/software/SPDF/utils/text/TextEncodingHelper.java b/app/core/src/main/java/stirling/software/SPDF/utils/text/TextEncodingHelper.java new file mode 100644 index 000000000..b67641d8c --- /dev/null +++ b/app/core/src/main/java/stirling/software/SPDF/utils/text/TextEncodingHelper.java @@ -0,0 +1,353 @@ +package stirling.software.SPDF.utils.text; + +import java.io.IOException; + +import org.apache.pdfbox.pdmodel.font.PDFont; +import org.apache.pdfbox.pdmodel.font.PDSimpleFont; +import org.apache.pdfbox.pdmodel.font.encoding.DictionaryEncoding; +import org.apache.pdfbox.pdmodel.font.encoding.Encoding; + +import lombok.extern.slf4j.Slf4j; + +import stirling.software.common.util.RegexPatternUtils; + +@Slf4j +public class TextEncodingHelper { + + public static boolean canEncodeCharacters(PDFont font, String text) { + if (font == null || text == null || text.isEmpty()) { + return false; + } + + try { + // Step 1: Primary check - full-string encoding (permissive for "good" cases) + byte[] encoded = font.encode(text); + if (encoded.length > 0) { + log.debug( + "Text '{}' has good full-string encoding for font {} - permissively allowing", + text, + font.getName() != null ? font.getName() : "Unknown"); + return true; + } + + // Step 2: Smart array-based fallback for TJ operator-style text + log.debug( + "Full encoding failed for '{}' - using array-based fallback for font {}", + text, + font.getName() != null ? font.getName() : "Unknown"); + + return validateAsCodePointArray(font, text); + + } catch (IOException | IllegalArgumentException e) { + log.debug( + "Encoding exception for text '{}' with font {} - trying array fallback: {}", + text, + font.getName() != null ? font.getName() : "Unknown", + e.getMessage()); + + if (isFontSubset(font.getName()) || hasCustomEncoding(font)) { + return validateAsCodePointArray(font, text); + } + + return false; // Non-subset fonts with encoding exceptions are likely problematic + } + } + + private static boolean validateAsCodePointArray(PDFont font, String text) { + int totalCodePoints = 0; + int successfulCodePoints = 0; + + // Iterate through code points (handles surrogates correctly per Unicode docs) + for (int i = 0; i < text.length(); ) { + int codePoint = text.codePointAt(i); + String charStr = new String(Character.toChars(codePoint)); + totalCodePoints++; + + try { + // Test encoding for this code point + byte[] charEncoded = font.encode(charStr); + if (charEncoded.length > 0) { + float charWidth = font.getStringWidth(charStr); + + if (charWidth >= 0) { + successfulCodePoints++; + log.debug( + "Code point '{}' (U+{}) encoded successfully", + charStr, + Integer.toHexString(codePoint).toUpperCase()); + } else { + log.debug( + "Code point '{}' (U+{}) has invalid width: {}", + charStr, + Integer.toHexString(codePoint).toUpperCase(), + charWidth); + } + } else { + log.debug( + "Code point '{}' (U+{}) encoding failed - empty result", + charStr, + Integer.toHexString(codePoint).toUpperCase()); + } + } catch (IOException | IllegalArgumentException e) { + log.debug( + "Code point '{}' (U+{}) validation failed: {}", + charStr, + Integer.toHexString(codePoint).toUpperCase(), + e.getMessage()); + } + + i += Character.charCount(codePoint); // Handle surrogates properly + } + + double successRate = + totalCodePoints > 0 ? (double) successfulCodePoints / totalCodePoints : 0; + boolean isAcceptable = successRate >= 0.95; + + log.debug( + "Array validation for '{}': {}/{} code points successful ({:.1f}%) - {}", + text, + successfulCodePoints, + totalCodePoints, + successRate * 100, + isAcceptable ? "ALLOWING" : "rejecting"); + + return isAcceptable; + } + + public static boolean isTextSegmentRemovable(PDFont font, String text) { + if (font == null || text == null || text.isEmpty()) { + return false; + } + + // Log the attempt + log.debug( + "Evaluating text segment for removal: '{}' with font {}", + text, + font.getName() != null ? font.getName() : "Unknown Font"); + + if (isSimpleCharacter(text)) { + try { + font.encode(text); + font.getStringWidth(text); + log.debug( + "Text '{}' is a simple character and passed validation - allowing removal", + text); + return true; + } catch (Exception e) { + log.debug( + "Simple character '{}' failed basic validation with font {}: {}", + text, + font.getName() != null ? font.getName() : "Unknown", + e.getMessage()); + return false; + } + } + + // For complex text, require comprehensive validation + return isTextFullyRemovable(font, text); + } + + public static boolean isTextFullyRemovable(PDFont font, String text) { + if (font == null || text == null || text.isEmpty()) { + return false; + } + + try { + // Check 1: Verify encoding capability using new smart approach + if (!canEncodeCharacters(font, text)) { + log.debug( + "Text '{}' failed encoding validation for font {}", + text, + font.getName() != null ? font.getName() : "Unknown"); + return false; + } + + // Check 2: Validate width calculation capability + float width = font.getStringWidth(text); + if (width < 0) { // Allow zero width (invisible chars) but reject negative (invalid) + log.debug( + "Text '{}' has invalid width {} for font {}", + text, + width, + font.getName() != null ? font.getName() : "Unknown"); + return false; // Invalid metrics prevent accurate removal + } + + // Check 3: Verify font descriptor completeness for redaction area calculation + if (font.getFontDescriptor() == null) { + log.debug( + "Missing font descriptor for font {}", + font.getName() != null ? font.getName() : "Unknown"); + return false; + } + + // Check 4: Test bounding box calculation for redaction area + try { + font.getFontDescriptor().getFontBoundingBox(); + } catch (IllegalArgumentException e) { + log.debug( + "Font bounding box unavailable for font {}: {}", + font.getName() != null ? font.getName() : "Unknown", + e.getMessage()); + return false; + } + + log.debug( + "Text '{}' passed comprehensive validation for font {}", + text, + font.getName() != null ? font.getName() : "Unknown"); + return true; + + } catch (IOException e) { + log.debug( + "Text '{}' failed validation for font {} due to IO error: {}", + text, + font.getName() != null ? font.getName() : "Unknown", + e.getMessage()); + return false; + } catch (IllegalArgumentException e) { + log.debug( + "Text '{}' failed validation for font {} due to argument error: {}", + text, + font.getName() != null ? font.getName() : "Unknown", + e.getMessage()); + return false; + } + } + + private static boolean isSimpleCharacter(String text) { + if (text == null || text.isEmpty()) { + return false; + } + + if (text.length() > 20) { + return false; + } + + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + + // Allow letters, digits, and whitespace (most common cases) + if (Character.isLetterOrDigit(c) || Character.isWhitespace(c)) { + continue; + } + + // Allow common ASCII punctuation + if (c >= 32 && c <= 126 && ".,!?;:()-[]{}\"'/@#$%&*+=<>|\\~`".indexOf(c) >= 0) { + continue; + } + + return false; + } + + return true; + } + + public static boolean hasCustomEncoding(PDFont font) { + try { + if (font instanceof PDSimpleFont simpleFont) { + try { + Encoding encoding = simpleFont.getEncoding(); + if (encoding != null) { + // Check for dictionary-based custom encodings + if (encoding instanceof DictionaryEncoding) { + log.debug("Font {} uses DictionaryEncoding (custom)", font.getName()); + return true; + } + + String encodingName = encoding.getClass().getSimpleName(); + if (encodingName.contains("Custom") + || encodingName.contains("Dictionary")) { + log.debug( + "Font {} uses custom encoding: {}", + font.getName(), + encodingName); + return true; + } + } + } catch (Exception e) { + log.debug( + "Encoding detection failed for font {}: {}", + font.getName(), + e.getMessage()); + return true; // Assume custom if detection fails + } + } + + if (font instanceof org.apache.pdfbox.pdmodel.font.PDType0Font) { + log.debug( + "Font {} is Type0 (CID) - generally uses standard CMaps", + font.getName() != null ? font.getName() : "Unknown"); + return false; + } + + log.debug( + "Font {} type {} - assuming standard encoding", + font.getName() != null ? font.getName() : "Unknown", + font.getClass().getSimpleName()); + return false; + + } catch (IllegalArgumentException e) { + log.debug( + "Custom encoding detection failed for font {}: {}", + font.getName() != null ? font.getName() : "Unknown", + e.getMessage()); + return false; // Be forgiving on detection failure + } + } + + public static boolean fontSupportsCharacter(PDFont font, String character) { + if (font == null || character == null || character.isEmpty()) { + return false; + } + + try { + byte[] encoded = font.encode(character); + if (encoded.length == 0) { + return false; + } + + float width = font.getStringWidth(character); + return width > 0; + + } catch (IOException | IllegalArgumentException e) { + log.debug( + "Character '{}' not supported by font {}: {}", + character, + font.getName() != null ? font.getName() : "Unknown", + e.getMessage()); + return false; + } + } + + public static boolean isFontSubset(String fontName) { + if (fontName == null) { + return false; + } + return RegexPatternUtils.getInstance().getFontNamePattern().matcher(fontName).matches(); + } + + public static boolean canCalculateBasicWidths(PDFont font) { + try { + float spaceWidth = font.getStringWidth(" "); + if (spaceWidth <= 0) { + return false; + } + + String[] testChars = {"a", "A", "0", ".", "e", "!"}; + for (String ch : testChars) { + try { + float width = font.getStringWidth(ch); + if (width > 0) { + return true; + } + } catch (IOException | IllegalArgumentException e) { + } + } + + return false; // Can't calculate width for any test characters + } catch (IOException | IllegalArgumentException e) { + return false; // Font failed basic width calculation + } + } +} diff --git a/app/core/src/main/java/stirling/software/SPDF/utils/text/TextFinderUtils.java b/app/core/src/main/java/stirling/software/SPDF/utils/text/TextFinderUtils.java new file mode 100644 index 000000000..3126f90ac --- /dev/null +++ b/app/core/src/main/java/stirling/software/SPDF/utils/text/TextFinderUtils.java @@ -0,0 +1,142 @@ +package stirling.software.SPDF.utils.text; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.regex.Pattern; + +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDResources; + +import lombok.extern.slf4j.Slf4j; + +import stirling.software.common.util.RegexPatternUtils; + +@Slf4j +public class TextFinderUtils { + + public static boolean validateFontReliability(org.apache.pdfbox.pdmodel.font.PDFont font) { + if (font == null) { + return false; + } + + if (font.isDamaged()) { + log.debug( + "Font {} is marked as damaged - using TextEncodingHelper validation", + font.getName()); + } + + if (TextEncodingHelper.canCalculateBasicWidths(font)) { + log.debug( + "Font {} passed basic width calculations - considering reliable", + font.getName()); + return true; + } + + String[] basicTests = {"1", "2", "3", "a", "A", "e", "E", " "}; + + int workingChars = 0; + for (String testChar : basicTests) { + if (TextEncodingHelper.canEncodeCharacters(font, testChar)) { + workingChars++; + } + } + + if (workingChars > 0) { + log.debug( + "Font {} can process {}/{} basic characters - considering reliable", + font.getName(), + workingChars, + basicTests.length); + return true; + } + + log.debug("Font {} failed all basic tests - considering unreliable", font.getName()); + return false; + } + + public static List createOptimizedSearchPatterns( + Set searchTerms, boolean useRegex, boolean wholeWordSearch) { + List patterns = new ArrayList<>(); + + for (String term : searchTerms) { + if (term == null || term.trim().isEmpty()) { + continue; + } + + try { + String patternString = useRegex ? term.trim() : Pattern.quote(term.trim()); + + if (wholeWordSearch) { + patternString = applyWordBoundaries(term.trim(), patternString); + } + + // Use PatternFactory for better performance with cached compilation + Pattern pattern = + RegexPatternUtils.getInstance().createSearchPattern(patternString, true); + patterns.add(pattern); + + log.debug("Created search pattern: '{}' -> '{}'", term.trim(), patternString); + + } catch (Exception e) { + log.warn("Failed to create pattern for term '{}': {}", term, e.getMessage()); + } + } + + return patterns; + } + + private static String applyWordBoundaries(String originalTerm, String patternString) { + if (originalTerm.length() == 1 && Character.isDigit(originalTerm.charAt(0))) { + return "(? 0 && (completelyUnusableFonts * 2 > totalFonts); + log.debug( + "Page font analysis: {}/{} fonts are completely unusable - page {} problematic", + completelyUnusableFonts, + totalFonts, + hasProblems ? "IS" : "is NOT"); + + return hasProblems; + + } catch (Exception e) { + log.warn("Font analysis failed for page: {}", e.getMessage()); + return false; // Be permissive if analysis fails + } + } +} diff --git a/app/core/src/main/java/stirling/software/SPDF/utils/text/WidthCalculator.java b/app/core/src/main/java/stirling/software/SPDF/utils/text/WidthCalculator.java new file mode 100644 index 000000000..fde3809c4 --- /dev/null +++ b/app/core/src/main/java/stirling/software/SPDF/utils/text/WidthCalculator.java @@ -0,0 +1,136 @@ +package stirling.software.SPDF.utils.text; + +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.font.PDFont; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class WidthCalculator { + + private static final int FONT_SCALE_FACTOR = 1000; + + public static float calculateAccurateWidth(PDFont font, String text, float fontSize) { + if (font == null || text == null || text.isEmpty() || fontSize <= 0) { + return 0; + } + + if (!TextEncodingHelper.canEncodeCharacters(font, text)) { + log.debug( + "Text cannot be encoded by font {}, using fallback width calculation", + font.getName()); + return calculateFallbackWidth(font, text, fontSize); + } + + try { + float rawWidth = font.getStringWidth(text); + float scaledWidth = (rawWidth / FONT_SCALE_FACTOR) * fontSize; + + log.debug( + "Direct width calculation successful for font {}: {} -> {}", + font.getName(), + rawWidth, + scaledWidth); + return scaledWidth; + + } catch (Exception e) { + log.debug( + "Direct width calculation failed for font {}: {}", + font.getName(), + e.getMessage()); + return calculateWidthWithCharacterIteration(font, text, fontSize); + } + } + + private static float calculateWidthWithCharacterIteration( + PDFont font, String text, float fontSize) { + try { + float totalWidth = 0; + + for (int i = 0; i < text.length(); i++) { + String character = text.substring(i, i + 1); + try { + byte[] encoded = font.encode(character); + if (encoded.length > 0) { + int glyphCode = encoded[0] & 0xFF; + float glyphWidth = font.getWidth(glyphCode); + + if (glyphWidth == 0) { + try { + glyphWidth = font.getWidthFromFont(glyphCode); + } catch (Exception e2) { + glyphWidth = font.getAverageFontWidth(); + } + } + + totalWidth += (glyphWidth / FONT_SCALE_FACTOR) * fontSize; + } else { + totalWidth += (font.getAverageFontWidth() / FONT_SCALE_FACTOR) * fontSize; + } + } catch (Exception e2) { + totalWidth += (font.getAverageFontWidth() / FONT_SCALE_FACTOR) * fontSize; + } + } + + log.debug("Character iteration width calculation: {}", totalWidth); + return totalWidth; + + } catch (Exception e) { + log.debug("Character iteration failed: {}", e.getMessage()); + return calculateFallbackWidth(font, text, fontSize); + } + } + + private static float calculateFallbackWidth(PDFont font, String text, float fontSize) { + try { + if (font.getFontDescriptor() != null + && font.getFontDescriptor().getFontBoundingBox() != null) { + + PDRectangle bbox = font.getFontDescriptor().getFontBoundingBox(); + float avgCharWidth = + bbox.getWidth() / FONT_SCALE_FACTOR * 0.6f; // Conservative estimate + float fallbackWidth = text.length() * avgCharWidth * fontSize; + + log.debug("Bounding box fallback width: {}", fallbackWidth); + return fallbackWidth; + } + + float avgWidth = font.getAverageFontWidth(); + float fallbackWidth = (text.length() * avgWidth / FONT_SCALE_FACTOR) * fontSize; + + log.debug("Average width fallback: {}", fallbackWidth); + return fallbackWidth; + + } catch (Exception e) { + float conservativeWidth = text.length() * 0.5f * fontSize; + log.debug( + "Conservative fallback width for font {}: {}", + font.getName(), + conservativeWidth); + return conservativeWidth; + } + } + + public static boolean isWidthCalculationReliable(PDFont font) { + if (font == null) { + return false; + } + + if (font.isDamaged()) { + log.debug("Font {} is damaged", font.getName()); + return false; + } + + if (!TextEncodingHelper.canCalculateBasicWidths(font)) { + log.debug("Font {} cannot perform basic width calculations", font.getName()); + return false; + } + + if (TextEncodingHelper.hasCustomEncoding(font)) { + log.debug("Font {} has custom encoding", font.getName()); + return false; + } + + return true; + } +} diff --git a/app/core/src/main/java/stirling/software/common/controller/JobController.java b/app/core/src/main/java/stirling/software/common/controller/JobController.java index 44b15265b..1a27e5264 100644 --- a/app/core/src/main/java/stirling/software/common/controller/JobController.java +++ b/app/core/src/main/java/stirling/software/common/controller/JobController.java @@ -10,8 +10,12 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; + import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; @@ -22,11 +26,14 @@ import stirling.software.common.model.job.ResultFile; import stirling.software.common.service.FileStorage; import stirling.software.common.service.JobQueue; import stirling.software.common.service.TaskManager; +import stirling.software.common.util.RegexPatternUtils; /** REST controller for job-related endpoints */ @RestController @RequiredArgsConstructor @Slf4j +@RequestMapping("/api/v1/general") +@Tag(name = "Job Management", description = "Job Management API") public class JobController { private final TaskManager taskManager; @@ -40,7 +47,8 @@ public class JobController { * @param jobId The job ID * @return The job result */ - @GetMapping("/api/v1/general/job/{jobId}") + @GetMapping("/job/{jobId}") + @Operation(summary = "Get job status") public ResponseEntity getJobStatus(@PathVariable("jobId") String jobId) { JobResult result = taskManager.getJobResult(jobId); if (result == null) { @@ -68,7 +76,8 @@ public class JobController { * @param jobId The job ID * @return The job result */ - @GetMapping("/api/v1/general/job/{jobId}/result") + @GetMapping("/job/{jobId}/result") + @Operation(summary = "Get job result") public ResponseEntity getJobResult(@PathVariable("jobId") String jobId) { JobResult result = taskManager.getJobResult(jobId); if (result == null) { @@ -130,7 +139,8 @@ public class JobController { * @param jobId The job ID * @return Response indicating whether the job was cancelled */ - @DeleteMapping("/api/v1/general/job/{jobId}") + @DeleteMapping("/job/{jobId}") + @Operation(summary = "Cancel a job") public ResponseEntity cancelJob(@PathVariable("jobId") String jobId) { log.debug("Request to cancel job: {}", jobId); @@ -197,7 +207,8 @@ public class JobController { * @param jobId The job ID * @return List of files for the job */ - @GetMapping("/api/v1/general/job/{jobId}/result/files") + @GetMapping("/job/{jobId}/result/files") + @Operation(summary = "Get job result files") public ResponseEntity getJobFiles(@PathVariable("jobId") String jobId) { JobResult result = taskManager.getJobResult(jobId); if (result == null) { @@ -226,7 +237,8 @@ public class JobController { * @param fileId The file ID * @return The file metadata */ - @GetMapping("/api/v1/general/files/{fileId}/metadata") + @GetMapping("/files/{fileId}/metadata") + @Operation(summary = "Get file metadata") public ResponseEntity getFileMetadata(@PathVariable("fileId") String fileId) { try { // Verify file exists @@ -249,7 +261,7 @@ public class JobController { "fileName", "unknown", "contentType", - "application/octet-stream", + MediaType.APPLICATION_OCTET_STREAM_VALUE, "fileSize", fileSize)); } @@ -266,7 +278,8 @@ public class JobController { * @param fileId The file ID * @return The file content */ - @GetMapping("/api/v1/general/files/{fileId}") + @GetMapping("/files/{fileId}") + @Operation(summary = "Download a file") public ResponseEntity downloadFile(@PathVariable("fileId") String fileId) { try { // Verify file exists @@ -283,7 +296,9 @@ public class JobController { String fileName = resultFile != null ? resultFile.getFileName() : "download"; String contentType = - resultFile != null ? resultFile.getContentType() : "application/octet-stream"; + resultFile != null + ? resultFile.getContentType() + : MediaType.APPLICATION_OCTET_STREAM_VALUE; return ResponseEntity.ok() .header("Content-Type", contentType) @@ -305,8 +320,10 @@ public class JobController { private String createContentDispositionHeader(String fileName) { try { String encodedFileName = - URLEncoder.encode(fileName, StandardCharsets.UTF_8) - .replace("+", "%20"); // URLEncoder uses + for spaces, but we want %20 + RegexPatternUtils.getInstance() + .getPlusSignPattern() + .matcher(URLEncoder.encode(fileName, StandardCharsets.UTF_8)) + .replaceAll("%20"); // URLEncoder uses + for spaces, but we want %20 return "attachment; filename=\"" + fileName + "\"; filename*=UTF-8''" + encodedFileName; } catch (Exception e) { // Fallback to basic filename if encoding fails diff --git a/app/core/src/main/resources/messages_ar_AR.properties b/app/core/src/main/resources/messages_ar_AR.properties index ed0bc1228..e8d48a831 100644 --- a/app/core/src/main/resources/messages_ar_AR.properties +++ b/app/core/src/main/resources/messages_ar_AR.properties @@ -137,6 +137,7 @@ lang.yor=Yoruba addPageNumbers.fontSize=حجم الخط addPageNumbers.fontName=اسم الخط +addPageNumbers.fontColor=Font Colour pdfPrompt=اختر PDF multiPdfPrompt=اختر ملفات PDF (2+) multiPdfDropPrompt=حدد (أو اسحب وأفلت) جميع ملفات PDF التي تحتاجها @@ -193,6 +194,7 @@ error.fileFormatRequired=File must be in {0} format error.invalidFormat=Invalid {0} format: {1} error.endpointDisabled=This endpoint has been disabled by the admin error.urlNotReachable=URL is not reachable, please provide a valid URL +error.invalidUrlFormat=Invalid URL format provided. The provided format is invalid. # DPI and image rendering messages - used by frontend for dynamic translation # Backend sends: [TRANSLATE:messageKey:arg1,arg2] English message @@ -604,6 +606,22 @@ home.imageToPdf.title=صورة إلى PDF home.imageToPdf.desc=تحويل الصور (PNG ، JPEG ، GIF) إلى PDF. imageToPdf.tags=تحويل,صورة,jpg,صورة,صورة فوتوغرافية +home.cbzToPdf.title=CBZ to PDF +home.cbzToPdf.desc=Convert CBZ comic book archives to PDF format. +cbzToPdf.tags=conversion,comic,book,archive,cbz,zip + +home.cbrToPdf.title=CBR to PDF +home.cbrToPdf.desc=Convert CBR comic book archives to PDF format. +cbrToPdf.tags=conversion,comic,book,archive,cbr,rar + +home.pdfToCbz.title=PDF to CBZ +home.pdfToCbz.desc=Convert PDF files to CBZ comic book archives. +pdfToCbz.tags=conversion,comic,book,archive,cbz,pdf + +home.pdfToCbr.title=PDF to CBR +home.pdfToCbr.desc=Convert PDF files to CBR comic book archives. +pdfToCbr.tags=conversion,comic,book,archive,cbr,rar + home.pdfToImage.title=تحويل PDF إلى صورة home.pdfToImage.desc=تحويل ملف PDF إلى صورة. (PNG ، JPEG ، GIF) pdfToImage.tags=تحويل,صورة,jpg,صورة,صورة فوتوغرافية @@ -876,6 +894,12 @@ replace-color.selectText.8=نص صفرة على خلفية سوداء replace-color.selectText.9=نص أخضر على خلفية سوداء replace-color.selectText.10=اختر لون النص replace-color.selectText.11=اختر لون الخلفية +replace-color.selectText.12=Colour Space Conversion (CMYK for Printing) +replace-color.selectText.13=CMYK Colour Space Conversion +replace-color.selectText.14=This option converts the PDF from RGB colour space to CMYK colour space, which is optimized for professional printing. This process: +replace-color.selectText.15=Converts colours to CMYK (Cyan, Magenta, Yellow, Black) colour model used by professional printers +replace-color.selectText.16=Optimizes the PDF for print production with prepress settings +replace-color.selectText.17=May result in slight colour changes as CMYK has a smaller colour gamut than RGB replace-color.submit=استبدال @@ -1081,7 +1105,7 @@ addPageNumbers.selectText.5=الصفحات المراد ترقيمها addPageNumbers.selectText.6=نص مخصص addPageNumbers.customTextDesc=نص مخصص addPageNumbers.numberPagesDesc=أي الصفحات المراد ترقيمها، الافتراضي 'الكل'، يقبل أيضًا 1-5 أو 2,5,9 إلخ -addPageNumbers.customNumberDesc=الافتراضي هو {n}، يقبل أيضًا 'الصفحة {n} من {total}'، 'نص-{n}'، '{filename}-{n} +addPageNumbers.customNumberDesc=الافتراضي هو {n}، يقبل أيضًا 'الصفحة {n} من {total}'، 'نص-{n}'، '{filename}-{n}' addPageNumbers.submit=إضافة أرقام الصفحات @@ -1217,6 +1241,7 @@ sign.previous=Previous page sign.maintainRatio=Toggle maintain aspect ratio sign.undo=Undo sign.redo=Redo +sign.colour=Signature Colour #repair repair.title=إصلاح @@ -1423,6 +1448,34 @@ imageToPDF.selectText.3=المنطق المتعدد للملفات (مفعل ف imageToPDF.selectText.4=دمج في ملف PDF واحد imageToPDF.selectText.5=تحويل إلى ملفات PDF منفصلة +#cbzToPDF +cbzToPDF.title=CBZ to PDF +cbzToPDF.header=CBZ to PDF +cbzToPDF.submit=Convert to PDF +cbzToPDF.selectText=Select CBZ file +cbzToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBZ +pdfToCBZ.title=PDF to CBZ +pdfToCBZ.header=PDF to CBZ +pdfToCBZ.submit=Convert to CBZ +pdfToCBZ.selectText=Select PDF file +pdfToCBZ.dpi=DPI (Dots Per Inch) + +#cbrToPDF +cbrToPDF.title=CBR to PDF +cbrToPDF.header=CBR to PDF +cbrToPDF.submit=Convert to PDF +cbrToPDF.selectText=Select CBR file +cbrToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBR +pdfToCBR.title=PDF to CBR +pdfToCBR.header=PDF to CBR +pdfToCBR.submit=Convert to CBR +pdfToCBR.selectText=Select PDF file +pdfToCBR.dpi=DPI (Dots Per Inch) +pdfToCBR.dpiHelp=Higher DPI results in better quality but larger file size. #pdfToImage pdfToImage.title=تحويل PDF إلى صورة @@ -1439,6 +1492,7 @@ pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=تحويل pdfToImage.info=Python غير مثبت. مطلوب لتحويل WebP. pdfToImage.placeholder=(مثال: 1,2,8 أو 4,7,12-16 أو 2n-1) +pdfToImage.includeAnnotations=Include annotations (comments, highlights etc.) #addPassword diff --git a/app/core/src/main/resources/messages_az_AZ.properties b/app/core/src/main/resources/messages_az_AZ.properties index f0e3f5ea9..5dc28f621 100644 --- a/app/core/src/main/resources/messages_az_AZ.properties +++ b/app/core/src/main/resources/messages_az_AZ.properties @@ -137,6 +137,7 @@ lang.yor=Yoruba addPageNumbers.fontSize=Şrift Ölçüsü addPageNumbers.fontName=Şrift Adı +addPageNumbers.fontColor=Font Colour pdfPrompt=PDF(lər)i Seç multiPdfPrompt=PDFləri Seç (2+) multiPdfDropPrompt=Ehtiyacınız olan bütün PDFləri seçin (və ya sürükləyib buraxın) @@ -193,6 +194,7 @@ error.fileFormatRequired=File must be in {0} format error.invalidFormat=Invalid {0} format: {1} error.endpointDisabled=This endpoint has been disabled by the admin error.urlNotReachable=URL is not reachable, please provide a valid URL +error.invalidUrlFormat=Invalid URL format provided. The provided format is invalid. # DPI and image rendering messages - used by frontend for dynamic translation # Backend sends: [TRANSLATE:messageKey:arg1,arg2] English message @@ -604,6 +606,22 @@ home.imageToPdf.title=Şəkildən PDF-ə home.imageToPdf.desc=Şəkli (PNG, JPEG, GIF) PDF-ə Çevir. imageToPdf.tags=çevirmə,şəkil,jpg,fotoşəkil,foto +home.cbzToPdf.title=CBZ to PDF +home.cbzToPdf.desc=Convert CBZ comic book archives to PDF format. +cbzToPdf.tags=conversion,comic,book,archive,cbz,zip + +home.cbrToPdf.title=CBR to PDF +home.cbrToPdf.desc=Convert CBR comic book archives to PDF format. +cbrToPdf.tags=conversion,comic,book,archive,cbr,rar + +home.pdfToCbz.title=PDF to CBZ +home.pdfToCbz.desc=Convert PDF files to CBZ comic book archives. +pdfToCbz.tags=conversion,comic,book,archive,cbz,pdf + +home.pdfToCbr.title=PDF to CBR +home.pdfToCbr.desc=Convert PDF files to CBR comic book archives. +pdfToCbr.tags=conversion,comic,book,archive,cbr,rar + home.pdfToImage.title=PDF-dən Şəkilə home.pdfToImage.desc=PDF-i Şəkilə Çevir. (PNG, JPEG, GIF) pdfToImage.tags=çevirmə,şəkil,jpg,fotoşəkil,foto @@ -876,6 +894,12 @@ replace-color.selectText.8=Qara arxaplanda sarı mətn replace-color.selectText.9=Qara arxaplanda yaşıl mətn replace-color.selectText.10=Mətn rəngi seç replace-color.selectText.11=Arxaplan rəngi seç +replace-color.selectText.12=Colour Space Conversion (CMYK for Printing) +replace-color.selectText.13=CMYK Colour Space Conversion +replace-color.selectText.14=This option converts the PDF from RGB colour space to CMYK colour space, which is optimized for professional printing. This process: +replace-color.selectText.15=Converts colours to CMYK (Cyan, Magenta, Yellow, Black) colour model used by professional printers +replace-color.selectText.16=Optimizes the PDF for print production with prepress settings +replace-color.selectText.17=May result in slight colour changes as CMYK has a smaller colour gamut than RGB replace-color.submit=Əvəzlə @@ -1081,7 +1105,7 @@ addPageNumbers.selectText.5=Səhifələrə nömrə əlavə edin addPageNumbers.selectText.6=Fərdi Mətn addPageNumbers.customTextDesc=Fərdi Mətn addPageNumbers.numberPagesDesc=Hansı səhifələrin nömrələnəcəyini seçin, default 'all', və ya 1-5, 2,5,9 kimi yazılış qəbul olunur -addPageNumbers.customNumberDesc=Defolt olaraq {n}, və ya 'Page {n} of {total}', 'Text-{n}', '{filename}-{n} +addPageNumbers.customNumberDesc=Defolt olaraq {n}, və ya 'Page {n} of {total}', 'Text-{n}', '{filename}-{n}' addPageNumbers.submit=Səhifə Nömrələri əlavə edin @@ -1217,6 +1241,7 @@ sign.previous=Əvvəlki səhifə sign.maintainRatio=Toggle maintain aspect ratio sign.undo=Undo sign.redo=Redo +sign.colour=Signature Colour #repair repair.title=Bərpa Et @@ -1423,6 +1448,34 @@ imageToPDF.selectText.3=Çoxsaylı Fayl Məntiqi (Yalnız Birdən Çox Şəkil imageToPDF.selectText.4=Tək Bir PDF-ə Birləşdir imageToPDF.selectText.5=Ayrı PDF-lərə Çevirin +#cbzToPDF +cbzToPDF.title=CBZ to PDF +cbzToPDF.header=CBZ to PDF +cbzToPDF.submit=Convert to PDF +cbzToPDF.selectText=Select CBZ file +cbzToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBZ +pdfToCBZ.title=PDF to CBZ +pdfToCBZ.header=PDF to CBZ +pdfToCBZ.submit=Convert to CBZ +pdfToCBZ.selectText=Select PDF file +pdfToCBZ.dpi=DPI (Dots Per Inch) + +#cbrToPDF +cbrToPDF.title=CBR to PDF +cbrToPDF.header=CBR to PDF +cbrToPDF.submit=Convert to PDF +cbrToPDF.selectText=Select CBR file +cbrToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBR +pdfToCBR.title=PDF to CBR +pdfToCBR.header=PDF to CBR +pdfToCBR.submit=Convert to CBR +pdfToCBR.selectText=Select PDF file +pdfToCBR.dpi=DPI (Dots Per Inch) +pdfToCBR.dpiHelp=Higher DPI results in better quality but larger file size. #pdfToImage pdfToImage.title=PDF-i Şəklə @@ -1439,6 +1492,7 @@ pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Çevir pdfToImage.info=Python Yüklü Deyil.WebP Çevirməsi Üçün Vacibdir pdfToImage.placeholder=(məsələn, 1,2,8 və ya 4,7,12-16 və ya 2n-1) +pdfToImage.includeAnnotations=Include annotations (comments, highlights etc.) #addPassword diff --git a/app/core/src/main/resources/messages_bg_BG.properties b/app/core/src/main/resources/messages_bg_BG.properties index d7964e792..76aae0b93 100644 --- a/app/core/src/main/resources/messages_bg_BG.properties +++ b/app/core/src/main/resources/messages_bg_BG.properties @@ -137,6 +137,7 @@ lang.yor=Yoruba addPageNumbers.fontSize=Размер на шрифт addPageNumbers.fontName=Име на шрифт +addPageNumbers.fontColor=Font Colour pdfPrompt=Изберете PDF(и) multiPdfPrompt=Изберете PDF (2+) multiPdfDropPrompt=Изберете (или плъзнете и пуснете) всички PDF файлове, от които се нуждаете @@ -193,6 +194,7 @@ error.fileFormatRequired=File must be in {0} format error.invalidFormat=Invalid {0} format: {1} error.endpointDisabled=This endpoint has been disabled by the admin error.urlNotReachable=URL is not reachable, please provide a valid URL +error.invalidUrlFormat=Invalid URL format provided. The provided format is invalid. # DPI and image rendering messages - used by frontend for dynamic translation # Backend sends: [TRANSLATE:messageKey:arg1,arg2] English message @@ -604,6 +606,22 @@ home.imageToPdf.title=Изображение към PDF home.imageToPdf.desc=Преобразуване на изображение (PNG, JPEG, GIF) към PDF. imageToPdf.tags=преобразуване,img,jpg,изображение,снимка +home.cbzToPdf.title=CBZ to PDF +home.cbzToPdf.desc=Convert CBZ comic book archives to PDF format. +cbzToPdf.tags=conversion,comic,book,archive,cbz,zip + +home.cbrToPdf.title=CBR to PDF +home.cbrToPdf.desc=Convert CBR comic book archives to PDF format. +cbrToPdf.tags=conversion,comic,book,archive,cbr,rar + +home.pdfToCbz.title=PDF to CBZ +home.pdfToCbz.desc=Convert PDF files to CBZ comic book archives. +pdfToCbz.tags=conversion,comic,book,archive,cbz,pdf + +home.pdfToCbr.title=PDF to CBR +home.pdfToCbr.desc=Convert PDF files to CBR comic book archives. +pdfToCbr.tags=conversion,comic,book,archive,cbr,rar + home.pdfToImage.title=PDF към изображение home.pdfToImage.desc=Преобразуване на PDF към изображение. (PNG, JPEG, GIF) pdfToImage.tags=преобразуване,img,jpg,изображение,снимка @@ -876,6 +894,12 @@ replace-color.selectText.8=Жълт текст на черен фон replace-color.selectText.9=Зелен текст на черен фон replace-color.selectText.10=Изберете цвят на текста replace-color.selectText.11=Изберете цвят на фона +replace-color.selectText.12=Colour Space Conversion (CMYK for Printing) +replace-color.selectText.13=CMYK Colour Space Conversion +replace-color.selectText.14=This option converts the PDF from RGB colour space to CMYK colour space, which is optimized for professional printing. This process: +replace-color.selectText.15=Converts colours to CMYK (Cyan, Magenta, Yellow, Black) colour model used by professional printers +replace-color.selectText.16=Optimizes the PDF for print production with prepress settings +replace-color.selectText.17=May result in slight colour changes as CMYK has a smaller colour gamut than RGB replace-color.submit=Замени @@ -1081,7 +1105,7 @@ addPageNumbers.selectText.5=Страници към номер addPageNumbers.selectText.6=Персонализиран текст addPageNumbers.customTextDesc=Персонализиран текст addPageNumbers.numberPagesDesc=Кои страници да номерирате, по подразбиране 'всички', също приема 1-5 или 2,5,9 и т.н. -addPageNumbers.customNumberDesc=По подразбиране е {n}, също приема 'Страница {n} от {total}', 'Текст-{n}', '{filename}-{n} +addPageNumbers.customNumberDesc=По подразбиране е {n}, също приема 'Страница {n} от {total}', 'Текст-{n}', '{filename}-{n}' addPageNumbers.submit=Добавяне на номера на страници @@ -1217,6 +1241,7 @@ sign.previous=Предишна стараница sign.maintainRatio=Превключване за поддържане на съотношението на страните sign.undo=Undo sign.redo=Redo +sign.colour=Signature Colour #repair repair.title=Поправи @@ -1423,6 +1448,34 @@ imageToPDF.selectText.3=Файлова логика с много (Активи imageToPDF.selectText.4=Сливане към един PDF imageToPDF.selectText.5=Преобразуване към отделни PDF файлове +#cbzToPDF +cbzToPDF.title=CBZ to PDF +cbzToPDF.header=CBZ to PDF +cbzToPDF.submit=Convert to PDF +cbzToPDF.selectText=Select CBZ file +cbzToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBZ +pdfToCBZ.title=PDF to CBZ +pdfToCBZ.header=PDF to CBZ +pdfToCBZ.submit=Convert to CBZ +pdfToCBZ.selectText=Select PDF file +pdfToCBZ.dpi=DPI (Dots Per Inch) + +#cbrToPDF +cbrToPDF.title=CBR to PDF +cbrToPDF.header=CBR to PDF +cbrToPDF.submit=Convert to PDF +cbrToPDF.selectText=Select CBR file +cbrToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBR +pdfToCBR.title=PDF to CBR +pdfToCBR.header=PDF to CBR +pdfToCBR.submit=Convert to CBR +pdfToCBR.selectText=Select PDF file +pdfToCBR.dpi=DPI (Dots Per Inch) +pdfToCBR.dpiHelp=Higher DPI results in better quality but larger file size. #pdfToImage pdfToImage.title=PDF към Изображение @@ -1439,6 +1492,7 @@ pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Преобразуване pdfToImage.info=Python не е инсталиран. Изисква се за конвертиране на WebP. pdfToImage.placeholder=(e.g. 1,2,8 or 4,7,12-16 or 2n-1) +pdfToImage.includeAnnotations=Include annotations (comments, highlights etc.) #addPassword diff --git a/app/core/src/main/resources/messages_bo_CN.properties b/app/core/src/main/resources/messages_bo_CN.properties index 32df39257..d937c76a9 100644 --- a/app/core/src/main/resources/messages_bo_CN.properties +++ b/app/core/src/main/resources/messages_bo_CN.properties @@ -137,6 +137,7 @@ lang.yor=Yoruba addPageNumbers.fontSize=ཡིག་གཟུགས་ཆེ་ཆུང་ addPageNumbers.fontName=ཡིག་གཟུགས་མིང་ +addPageNumbers.fontColor=Font Colour pdfPrompt=PDF འདེམས་རོགས། multiPdfPrompt=PDF གཉིས་ཡན་འདེམས་རོགས། multiPdfDropPrompt=དགོས་མཁོ་འདི་ PDF ཡིག་ཆ་ཚང་མ་འདེམས་པའམ་འཐེན་རོགས། @@ -193,6 +194,7 @@ error.fileFormatRequired=File must be in {0} format error.invalidFormat=Invalid {0} format: {1} error.endpointDisabled=This endpoint has been disabled by the admin error.urlNotReachable=URL is not reachable, please provide a valid URL +error.invalidUrlFormat=Invalid URL format provided. The provided format is invalid. # DPI and image rendering messages - used by frontend for dynamic translation # Backend sends: [TRANSLATE:messageKey:arg1,arg2] English message @@ -604,6 +606,22 @@ home.imageToPdf.title=པར་རིས་ནས་ PDF ལ། home.imageToPdf.desc=པར་རིས་ (PNG, JPEG, GIF) ནས་ PDF ལ་བསྒྱུར་བ། imageToPdf.tags=བསྒྱུར་བ།,པར་རིས།,jpg,པར།,འདྲ་པར། +home.cbzToPdf.title=CBZ to PDF +home.cbzToPdf.desc=Convert CBZ comic book archives to PDF format. +cbzToPdf.tags=conversion,comic,book,archive,cbz,zip + +home.cbrToPdf.title=CBR to PDF +home.cbrToPdf.desc=Convert CBR comic book archives to PDF format. +cbrToPdf.tags=conversion,comic,book,archive,cbr,rar + +home.pdfToCbz.title=PDF to CBZ +home.pdfToCbz.desc=Convert PDF files to CBZ comic book archives. +pdfToCbz.tags=conversion,comic,book,archive,cbz,pdf + +home.pdfToCbr.title=PDF to CBR +home.pdfToCbr.desc=Convert PDF files to CBR comic book archives. +pdfToCbr.tags=conversion,comic,book,archive,cbr,rar + home.pdfToImage.title=PDF ནས་པར་རིས་ལ། home.pdfToImage.desc=PDF ནས་པར་རིས་ (PNG, JPEG, GIF) ལ་བསྒྱུར་བ། pdfToImage.tags=བསྒྱུར་བ།,པར་རིས།,jpg,པར།,འདྲ་པར། @@ -876,6 +894,12 @@ replace-color.selectText.8=རྒྱབ་ལྗོངས་ནག་པོའ replace-color.selectText.9=རྒྱབ་ལྗོངས་ནག་པོའི་སྟེང་གི་ཡི་གེ་ལྗང་ཁུ། replace-color.selectText.10=ཡི་གེའི་ཚོས་གཞི་འདེམས་པ། replace-color.selectText.11=རྒྱབ་ལྗོངས་ཀྱི་ཚོས་གཞི་འདེམས་པ། +replace-color.selectText.12=Colour Space Conversion (CMYK for Printing) +replace-color.selectText.13=CMYK Colour Space Conversion +replace-color.selectText.14=This option converts the PDF from RGB colour space to CMYK colour space, which is optimized for professional printing. This process: +replace-color.selectText.15=Converts colours to CMYK (Cyan, Magenta, Yellow, Black) colour model used by professional printers +replace-color.selectText.16=Optimizes the PDF for print production with prepress settings +replace-color.selectText.17=May result in slight colour changes as CMYK has a smaller colour gamut than RGB replace-color.submit=བརྗེ་སྒྱུར། @@ -1217,6 +1241,7 @@ sign.previous=ཤོག་ངོས་སྔོན་མ། sign.maintainRatio=བསྡུར་ཚད་རྒྱུན་འཁྱོངས་སྒོ་རྒྱག་པ། sign.undo=Undo sign.redo=Redo +sign.colour=Signature Colour #repair repair.title=བཟོ་བཅོས། @@ -1423,6 +1448,34 @@ imageToPDF.selectText.3=ཡིག་ཆ་མང་པོའི་གཏན་ imageToPDF.selectText.4=PDF གཅིག་ཏུ་སྡེབ་སྦྱོར། imageToPDF.selectText.5=PDF སོ་སོར་བསྒྱུར་བ། +#cbzToPDF +cbzToPDF.title=CBZ to PDF +cbzToPDF.header=CBZ to PDF +cbzToPDF.submit=Convert to PDF +cbzToPDF.selectText=Select CBZ file +cbzToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBZ +pdfToCBZ.title=PDF to CBZ +pdfToCBZ.header=PDF to CBZ +pdfToCBZ.submit=Convert to CBZ +pdfToCBZ.selectText=Select PDF file +pdfToCBZ.dpi=DPI (Dots Per Inch) + +#cbrToPDF +cbrToPDF.title=CBR to PDF +cbrToPDF.header=CBR to PDF +cbrToPDF.submit=Convert to PDF +cbrToPDF.selectText=Select CBR file +cbrToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBR +pdfToCBR.title=PDF to CBR +pdfToCBR.header=PDF to CBR +pdfToCBR.submit=Convert to CBR +pdfToCBR.selectText=Select PDF file +pdfToCBR.dpi=DPI (Dots Per Inch) +pdfToCBR.dpiHelp=Higher DPI results in better quality but larger file size. #pdfToImage pdfToImage.title=PDF ནས་པར་རིས་ལ། @@ -1439,6 +1492,7 @@ pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=བསྒྱུར་བ། pdfToImage.info=Python སྒྲིག་འཇུག་བྱས་མི་འདུག WebP བསྒྱུར་བར་དགོས་མཁོ་ཡིན། pdfToImage.placeholder=(དཔེར་ན། 1,2,8 ཡང་ན་ 4,7,12-16 ཡང་ན་ 2n-1) +pdfToImage.includeAnnotations=Include annotations (comments, highlights etc.) #addPassword diff --git a/app/core/src/main/resources/messages_ca_CA.properties b/app/core/src/main/resources/messages_ca_CA.properties index dda522bdd..02e8458a0 100644 --- a/app/core/src/main/resources/messages_ca_CA.properties +++ b/app/core/src/main/resources/messages_ca_CA.properties @@ -137,6 +137,7 @@ lang.yor=Yoruba addPageNumbers.fontSize=Mida del tipus de lletra addPageNumbers.fontName=Nom del tipus de lletra +addPageNumbers.fontColor=Font Colour pdfPrompt=Selecciona PDF(s) multiPdfPrompt=Selecciona PDFs (2+) multiPdfDropPrompt=Selecciona (o arrossega) els documents PDF @@ -193,6 +194,7 @@ error.fileFormatRequired=File must be in {0} format error.invalidFormat=Invalid {0} format: {1} error.endpointDisabled=This endpoint has been disabled by the admin error.urlNotReachable=URL is not reachable, please provide a valid URL +error.invalidUrlFormat=Invalid URL format provided. The provided format is invalid. # DPI and image rendering messages - used by frontend for dynamic translation # Backend sends: [TRANSLATE:messageKey:arg1,arg2] English message @@ -604,6 +606,22 @@ home.imageToPdf.title=Imatge a PDF home.imageToPdf.desc=Converteix imatge (PNG, JPEG, GIF) a PDF. imageToPdf.tags=conversió,img,jpg,imatge,foto +home.cbzToPdf.title=CBZ to PDF +home.cbzToPdf.desc=Convert CBZ comic book archives to PDF format. +cbzToPdf.tags=conversion,comic,book,archive,cbz,zip + +home.cbrToPdf.title=CBR to PDF +home.cbrToPdf.desc=Convert CBR comic book archives to PDF format. +cbrToPdf.tags=conversion,comic,book,archive,cbr,rar + +home.pdfToCbz.title=PDF to CBZ +home.pdfToCbz.desc=Convert PDF files to CBZ comic book archives. +pdfToCbz.tags=conversion,comic,book,archive,cbz,pdf + +home.pdfToCbr.title=PDF to CBR +home.pdfToCbr.desc=Convert PDF files to CBR comic book archives. +pdfToCbr.tags=conversion,comic,book,archive,cbr,rar + home.pdfToImage.title=PDF a Imatge home.pdfToImage.desc=Converteix PDF a imatge (PNG, JPEG, GIF) pdfToImage.tags=conversió,img,jpg,imatge,foto @@ -876,6 +894,12 @@ replace-color.selectText.8=Text groc sobre fons negre replace-color.selectText.9=Text verd sobre fons negre replace-color.selectText.10=Tria el color del text replace-color.selectText.11=Tria el color del fons +replace-color.selectText.12=Colour Space Conversion (CMYK for Printing) +replace-color.selectText.13=CMYK Colour Space Conversion +replace-color.selectText.14=This option converts the PDF from RGB colour space to CMYK colour space, which is optimized for professional printing. This process: +replace-color.selectText.15=Converts colours to CMYK (Cyan, Magenta, Yellow, Black) colour model used by professional printers +replace-color.selectText.16=Optimizes the PDF for print production with prepress settings +replace-color.selectText.17=May result in slight colour changes as CMYK has a smaller colour gamut than RGB replace-color.submit=Reemplaça @@ -1217,6 +1241,7 @@ sign.previous=Previous page sign.maintainRatio=Toggle maintain aspect ratio sign.undo=Undo sign.redo=Redo +sign.colour=Signature Colour #repair repair.title=Reparar @@ -1423,6 +1448,34 @@ imageToPDF.selectText.3=Lògica de diversos fitxers (només està activada si es imageToPDF.selectText.4=Combina en un únic PDF imageToPDF.selectText.5=Converteix per separar els PDFs +#cbzToPDF +cbzToPDF.title=CBZ to PDF +cbzToPDF.header=CBZ to PDF +cbzToPDF.submit=Convert to PDF +cbzToPDF.selectText=Select CBZ file +cbzToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBZ +pdfToCBZ.title=PDF to CBZ +pdfToCBZ.header=PDF to CBZ +pdfToCBZ.submit=Convert to CBZ +pdfToCBZ.selectText=Select PDF file +pdfToCBZ.dpi=DPI (Dots Per Inch) + +#cbrToPDF +cbrToPDF.title=CBR to PDF +cbrToPDF.header=CBR to PDF +cbrToPDF.submit=Convert to PDF +cbrToPDF.selectText=Select CBR file +cbrToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBR +pdfToCBR.title=PDF to CBR +pdfToCBR.header=PDF to CBR +pdfToCBR.submit=Convert to CBR +pdfToCBR.selectText=Select PDF file +pdfToCBR.dpi=DPI (Dots Per Inch) +pdfToCBR.dpiHelp=Higher DPI results in better quality but larger file size. #pdfToImage pdfToImage.title=PDF a Imatge @@ -1439,6 +1492,7 @@ pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Converteix pdfToImage.info=Python no està instal·lat. És necessari per a la conversió a WebP. pdfToImage.placeholder=(p. ex. 1,2,8 o 4,7,12-16 o 2n-1) +pdfToImage.includeAnnotations=Include annotations (comments, highlights etc.) #addPassword diff --git a/app/core/src/main/resources/messages_cs_CZ.properties b/app/core/src/main/resources/messages_cs_CZ.properties index 7ce4b77a2..ba1587a84 100644 --- a/app/core/src/main/resources/messages_cs_CZ.properties +++ b/app/core/src/main/resources/messages_cs_CZ.properties @@ -137,6 +137,7 @@ lang.yor=Yoruba addPageNumbers.fontSize=Velikost písma addPageNumbers.fontName=Název písma +addPageNumbers.fontColor=Font Colour pdfPrompt=Vyberte PDF soubor(y) multiPdfPrompt=Vyberte PDF soubory (2+) multiPdfDropPrompt=Vyberte (nebo přetáhněte) všechny požadované PDF soubory @@ -193,6 +194,7 @@ error.fileFormatRequired=File must be in {0} format error.invalidFormat=Invalid {0} format: {1} error.endpointDisabled=This endpoint has been disabled by the admin error.urlNotReachable=URL is not reachable, please provide a valid URL +error.invalidUrlFormat=Invalid URL format provided. The provided format is invalid. # DPI and image rendering messages - used by frontend for dynamic translation # Backend sends: [TRANSLATE:messageKey:arg1,arg2] English message @@ -604,6 +606,22 @@ home.imageToPdf.title=Obrázek na PDF home.imageToPdf.desc=Převést obrázek (PNG, JPEG, GIF) na PDF. imageToPdf.tags=převod,img,jpg,obrázek,fotka +home.cbzToPdf.title=CBZ to PDF +home.cbzToPdf.desc=Convert CBZ comic book archives to PDF format. +cbzToPdf.tags=conversion,comic,book,archive,cbz,zip + +home.cbrToPdf.title=CBR to PDF +home.cbrToPdf.desc=Convert CBR comic book archives to PDF format. +cbrToPdf.tags=conversion,comic,book,archive,cbr,rar + +home.pdfToCbz.title=PDF to CBZ +home.pdfToCbz.desc=Convert PDF files to CBZ comic book archives. +pdfToCbz.tags=conversion,comic,book,archive,cbz,pdf + +home.pdfToCbr.title=PDF to CBR +home.pdfToCbr.desc=Convert PDF files to CBR comic book archives. +pdfToCbr.tags=conversion,comic,book,archive,cbr,rar + home.pdfToImage.title=PDF na obrázek home.pdfToImage.desc=Převést PDF na obrázek. (PNG, JPEG, GIF) pdfToImage.tags=převod,img,jpg,obrázek,fotka @@ -876,6 +894,12 @@ replace-color.selectText.8=Žlutý text na černém pozadí replace-color.selectText.9=Zelený text na černém pozadí replace-color.selectText.10=Vybrat barvu textu replace-color.selectText.11=Vybrat barvu pozadí +replace-color.selectText.12=Colour Space Conversion (CMYK for Printing) +replace-color.selectText.13=CMYK Colour Space Conversion +replace-color.selectText.14=This option converts the PDF from RGB colour space to CMYK colour space, which is optimized for professional printing. This process: +replace-color.selectText.15=Converts colours to CMYK (Cyan, Magenta, Yellow, Black) colour model used by professional printers +replace-color.selectText.16=Optimizes the PDF for print production with prepress settings +replace-color.selectText.17=May result in slight colour changes as CMYK has a smaller colour gamut than RGB replace-color.submit=Nahradit @@ -1217,6 +1241,7 @@ sign.previous=Předchozí stránka sign.maintainRatio=Přepnout zachování poměru stran sign.undo=Undo sign.redo=Redo +sign.colour=Signature Colour #repair repair.title=Opravit @@ -1423,6 +1448,34 @@ imageToPDF.selectText.3=Logika pro více souborů (Povoleno pouze při práci s imageToPDF.selectText.4=Sloučit do jednoho PDF imageToPDF.selectText.5=Převést na samostatné PDF +#cbzToPDF +cbzToPDF.title=CBZ to PDF +cbzToPDF.header=CBZ to PDF +cbzToPDF.submit=Convert to PDF +cbzToPDF.selectText=Select CBZ file +cbzToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBZ +pdfToCBZ.title=PDF to CBZ +pdfToCBZ.header=PDF to CBZ +pdfToCBZ.submit=Convert to CBZ +pdfToCBZ.selectText=Select PDF file +pdfToCBZ.dpi=DPI (Dots Per Inch) + +#cbrToPDF +cbrToPDF.title=CBR to PDF +cbrToPDF.header=CBR to PDF +cbrToPDF.submit=Convert to PDF +cbrToPDF.selectText=Select CBR file +cbrToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBR +pdfToCBR.title=PDF to CBR +pdfToCBR.header=PDF to CBR +pdfToCBR.submit=Convert to CBR +pdfToCBR.selectText=Select PDF file +pdfToCBR.dpi=DPI (Dots Per Inch) +pdfToCBR.dpiHelp=Higher DPI results in better quality but larger file size. #pdfToImage pdfToImage.title=PDF na obrázek @@ -1439,6 +1492,7 @@ pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Převést pdfToImage.info=Python není nainstalován. Vyžadován pro konverzi do WebP. pdfToImage.placeholder=(např. 1,2,8 nebo 4,7,12-16 nebo 2n-1) +pdfToImage.includeAnnotations=Include annotations (comments, highlights etc.) #addPassword diff --git a/app/core/src/main/resources/messages_da_DK.properties b/app/core/src/main/resources/messages_da_DK.properties index b82f1d761..3cc680e1c 100644 --- a/app/core/src/main/resources/messages_da_DK.properties +++ b/app/core/src/main/resources/messages_da_DK.properties @@ -137,6 +137,7 @@ lang.yor=Yoruba addPageNumbers.fontSize=Skriftstørrelse addPageNumbers.fontName=Skriftnavn +addPageNumbers.fontColor=Font Colour pdfPrompt=Vælg PDF-fil(er) multiPdfPrompt=Vælg PDF-filerne (2+) multiPdfDropPrompt=Vælg (eller drag & drop) alle PDF-filerne du skal bruge @@ -193,6 +194,7 @@ error.fileFormatRequired=File must be in {0} format error.invalidFormat=Invalid {0} format: {1} error.endpointDisabled=This endpoint has been disabled by the admin error.urlNotReachable=URL is not reachable, please provide a valid URL +error.invalidUrlFormat=Invalid URL format provided. The provided format is invalid. # DPI and image rendering messages - used by frontend for dynamic translation # Backend sends: [TRANSLATE:messageKey:arg1,arg2] English message @@ -604,6 +606,22 @@ home.imageToPdf.title=Billede til PDF home.imageToPdf.desc=Konvertér et billede (PNG, JPEG, GIF) til PDF. imageToPdf.tags=konvertering,img,jpg,billede,foto +home.cbzToPdf.title=CBZ to PDF +home.cbzToPdf.desc=Convert CBZ comic book archives to PDF format. +cbzToPdf.tags=conversion,comic,book,archive,cbz,zip + +home.cbrToPdf.title=CBR to PDF +home.cbrToPdf.desc=Convert CBR comic book archives to PDF format. +cbrToPdf.tags=conversion,comic,book,archive,cbr,rar + +home.pdfToCbz.title=PDF to CBZ +home.pdfToCbz.desc=Convert PDF files to CBZ comic book archives. +pdfToCbz.tags=conversion,comic,book,archive,cbz,pdf + +home.pdfToCbr.title=PDF to CBR +home.pdfToCbr.desc=Convert PDF files to CBR comic book archives. +pdfToCbr.tags=conversion,comic,book,archive,cbr,rar + home.pdfToImage.title=PDF til Billede home.pdfToImage.desc=Konvertér en PDF til et billede. (PNG, JPEG, GIF) pdfToImage.tags=konvertering,img,jpg,billede,foto @@ -876,6 +894,12 @@ replace-color.selectText.8=Gul tekst på sort baggrund replace-color.selectText.9=Grøn tekst på sort baggrund replace-color.selectText.10=Vælg tekstfarve replace-color.selectText.11=Vælg baggrundsfarve +replace-color.selectText.12=Colour Space Conversion (CMYK for Printing) +replace-color.selectText.13=CMYK Colour Space Conversion +replace-color.selectText.14=This option converts the PDF from RGB colour space to CMYK colour space, which is optimized for professional printing. This process: +replace-color.selectText.15=Converts colours to CMYK (Cyan, Magenta, Yellow, Black) colour model used by professional printers +replace-color.selectText.16=Optimizes the PDF for print production with prepress settings +replace-color.selectText.17=May result in slight colour changes as CMYK has a smaller colour gamut than RGB replace-color.submit=Erstat @@ -1081,7 +1105,7 @@ addPageNumbers.selectText.5=Sider at nummerere addPageNumbers.selectText.6=Brugerdefineret Tekst addPageNumbers.customTextDesc=Brugerdefineret Tekst addPageNumbers.numberPagesDesc=Hvilke sider der skal nummereres, standard 'alle', accepterer også 1-5 eller 2,5,9 osv. -addPageNumbers.customNumberDesc=Standard er {n}, accepterer også 'Side {n} af {total}', 'Tekst-{n}', '{filnavn}-{n} +addPageNumbers.customNumberDesc=Standard er {n}, accepterer også 'Side {n} af {total}', 'Tekst-{n}', '{filename}-{n}' addPageNumbers.submit=Tilføj Sidenumre @@ -1217,6 +1241,7 @@ sign.previous=Previous page sign.maintainRatio=Toggle maintain aspect ratio sign.undo=Undo sign.redo=Redo +sign.colour=Signature Colour #repair repair.title=Reparér @@ -1423,6 +1448,34 @@ imageToPDF.selectText.3=Multi-fil logik (Kun aktiveret ved arbejde med flere bil imageToPDF.selectText.4=Flet til enkelt PDF imageToPDF.selectText.5=Konvertér til separate PDF'er +#cbzToPDF +cbzToPDF.title=CBZ to PDF +cbzToPDF.header=CBZ to PDF +cbzToPDF.submit=Convert to PDF +cbzToPDF.selectText=Select CBZ file +cbzToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBZ +pdfToCBZ.title=PDF to CBZ +pdfToCBZ.header=PDF to CBZ +pdfToCBZ.submit=Convert to CBZ +pdfToCBZ.selectText=Select PDF file +pdfToCBZ.dpi=DPI (Dots Per Inch) + +#cbrToPDF +cbrToPDF.title=CBR to PDF +cbrToPDF.header=CBR to PDF +cbrToPDF.submit=Convert to PDF +cbrToPDF.selectText=Select CBR file +cbrToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBR +pdfToCBR.title=PDF to CBR +pdfToCBR.header=PDF to CBR +pdfToCBR.submit=Convert to CBR +pdfToCBR.selectText=Select PDF file +pdfToCBR.dpi=DPI (Dots Per Inch) +pdfToCBR.dpiHelp=Higher DPI results in better quality but larger file size. #pdfToImage pdfToImage.title=PDF til Billede @@ -1439,6 +1492,7 @@ pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Konvertér pdfToImage.info=Python er ikke installeret. Påkrævet for WebP-konvertering. pdfToImage.placeholder=(f.eks. 1,2,8 eller 4,7,12-16 eller 2n-1) +pdfToImage.includeAnnotations=Include annotations (comments, highlights etc.) #addPassword diff --git a/app/core/src/main/resources/messages_de_DE.properties b/app/core/src/main/resources/messages_de_DE.properties index db91b8dc7..170f94e4c 100644 --- a/app/core/src/main/resources/messages_de_DE.properties +++ b/app/core/src/main/resources/messages_de_DE.properties @@ -137,6 +137,7 @@ lang.yor=Yoruba addPageNumbers.fontSize=Schriftgröße addPageNumbers.fontName=Schriftart +addPageNumbers.fontColor=Schriftfarbe pdfPrompt=PDF(s) auswählen multiPdfPrompt=PDFs auswählen (2+) multiPdfDropPrompt=Wählen Sie alle gewünschten PDFs aus (oder ziehen Sie sie per Drag & Drop hierhin) @@ -193,6 +194,7 @@ error.fileFormatRequired=Die Datei muss im Format {0} vorliegen. error.invalidFormat=Ungültiges {0}-Format: {1} error.endpointDisabled=Dieser Endpunkt wurde vom Administrator deaktiviert. error.urlNotReachable=Die URL ist nicht erreichbar, bitte geben Sie eine gültige URL an. +error.invalidUrlFormat=Ungültiges URL-Format angegeben. Das angegebene Format ist ungültig. # DPI and image rendering messages - used by frontend for dynamic translation # Backend sends: [TRANSLATE:messageKey:arg1,arg2] English message @@ -368,36 +370,36 @@ settings.update=Update verfügbar settings.updateAvailable={0} ist die aktuelle installierte Version. Eine neue Version ({1}) ist verfügbar. # Update modal and notification strings -update.urgentUpdateAvailable=🚨 Update Available -update.updateAvailable=Update Available -update.modalTitle=Update Available -update.current=Current -update.latest=Latest -update.latestStable=Latest Stable -update.priority=Priority -update.recommendedAction=Recommended Action -update.breakingChangesDetected=⚠️ Breaking Changes Detected -update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. -update.migrationGuides=Migration Guides: -update.viewGuide=View Guide -update.loadingDetailedInfo=Loading detailed version information... -update.close=Close -update.viewAllReleases=View All Releases -update.downloadLatest=Download Latest -update.availableUpdates=Available Updates: -update.unableToLoadDetails=Unable to load detailed version information. +update.urgentUpdateAvailable=🚨 U +update.updateAvailable=Update verfügbar +update.modalTitle=Update verfügbar +update.current=Aktuell +update.latest=Neueste +update.latestStable=Neueste stabile +update.priority=Priorität +update.recommendedAction=Empfohlene Aktion +update.breakingChangesDetected=⚠️ Breaking-Change erkannt +update.breakingChangesMessage=Dieses Update enthält Breaking-Change. Bitte überprüfen Sie die Migrationsanleitungen unten. +update.migrationGuides=Migrationsanleitungen: +update.viewGuide=Anleitung anzeigen +update.loadingDetailedInfo=Lade detaillierte Versionsinformationen... +update.close=Schließen +update.viewAllReleases=Alle Versionen anzeigen +update.downloadLatest=Neueste herunterladen +update.availableUpdates=Verfügbare Updates: +update.unableToLoadDetails=Details zur Version konnten nicht geladen werden. update.version=Version # Update priority levels -update.priority.urgent=URGENT -update.priority.normal=NORMAL -update.priority.minor=MINOR -update.priority.low=LOW +update.priority.urgent=DRINGEND +update.priority.normal=REGULÄR +update.priority.minor=UNERHEBLICH +update.priority.low=GERING # Breaking changes text -update.breakingChanges=Breaking Changes: -update.breakingChangesDefault=This version contains breaking changes -update.migrationGuide=Migration Guide +update.breakingChanges=Breaking-Changes: +update.breakingChangesDefault=Diese Version enthält Breaking-Changes +update.migrationGuide=Migrationsanleitung: settings.appVersion=App-Version: settings.downloadOption.title=Download-Option wählen (für einzelne Dateien, die keine Zip-Downloads sind): settings.downloadOption.1=Im selben Fenster öffnen @@ -604,6 +606,22 @@ home.imageToPdf.title=Bild zu PDF home.imageToPdf.desc=Konvertieren Sie ein Bild (PNG, JPEG, GIF) in ein PDF imageToPdf.tags=konvertierung,img,jpg,bild,foto +home.cbzToPdf.title=CBZ zu PDF +home.cbzToPdf.desc=CBZ-Comicarchive in das PDF-Format konvertieren. +cbzToPdf.tags=konvertierung,comic,buch,archiv,cbz,zip + +home.cbrToPdf.title=CBR zu PDF +home.cbrToPdf.desc=CBR-Comicarchive in das PDF-Format konvertieren. +cbrToPdf.tags=konvertierung,comic,buch,archiv,cbr,rar + +home.pdfToCbz.title=PDF zu CBZ +home.pdfToCbz.desc=PDF-Dateien in CBZ-Comicarchive umwandeln. +pdfToCbz.tags=konvertierung,comic,buch,archiv,cbz,pdf + +home.pdfToCbr.title=PDF zu CBR +home.pdfToCbr.desc=PDF-Dateien in CBR-Comicarchive umwandeln. +pdfToCbr.tags=konvertierung,comic,buch,archiv,cbr,rar + home.pdfToImage.title=PDF zu Bild home.pdfToImage.desc=Konvertieren Sie ein PDF in ein Bild (PNG, JPEG, GIF) pdfToImage.tags=konvertierung,img,jpg,bild,foto @@ -876,6 +894,12 @@ replace-color.selectText.8=Gelber Text auf schwarzem Hintergrund replace-color.selectText.9=Grüner Text auf schwarzem Hintergrund replace-color.selectText.10=Textfarbe auswählen replace-color.selectText.11=Hintergrundfarbe auswählen +replace-color.selectText.12=Farbkonvertierung (CMYK für den Druck) +replace-color.selectText.13=CMYK-Farbkonvertierung +replace-color.selectText.14=Diese Option konvertiert die PDF von RGB-Farbraum in CMYK-Farbraum, der für den professionellen Druck optimiert ist. Dieser Prozess: +replace-color.selectText.15=Konvertiert Farben in das CMYK-Farbmodell (Cyan, Magenta, Gelb, Schwarz), das von professionellen Druckern verwendet wird +replace-color.selectText.16=Optimiert die PDF für die Druckproduktion mit Druckvorstufeinstellungen +replace-color.selectText.17=Kann zu leichten Farbänderungen führen, da CMYK einen kleineren Farbraum als RGB hat replace-color.submit=Ersetzen @@ -908,7 +932,7 @@ login.alreadyLoggedIn=Sie sind bereits an login.alreadyLoggedIn2=Geräten angemeldet. Bitte melden Sie sich dort ab und versuchen es dann erneut. login.toManySessions=Sie haben zu viele aktive Sitzungen login.logoutMessage=Sie wurden erfolgreich abgemeldet. -login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator. +login.invalidInResponseTo=Die angeforderte SAML-Antwort ist ungültig oder abgelaufen. Bitte wenden Sie sich an den Administrator. #auto-redact autoRedact.title=Automatisch zensieren/schwärzen @@ -1217,6 +1241,7 @@ sign.previous=Vorherige Seite sign.maintainRatio=Seitenverhältnis beibehalten ein-/ausschalten sign.undo=Rückgängig sign.redo=Wiederherstellen +sign.colour=Signaturfarbe #repair repair.title=Reparieren @@ -1423,6 +1448,34 @@ imageToPDF.selectText.3=Mehrere Dateien verarbeiten (nur aktiv, wenn Sie mit meh imageToPDF.selectText.4=In ein einziges PDF zusammenführen imageToPDF.selectText.5=In separate PDFs konvertieren +#cbzToPDF +cbzToPDF.title=CBZ zu PDF +cbzToPDF.header=CBZ zu PDF +cbzToPDF.submit=Zu PDF konvertieren +cbzToPDF.selectText=CBZ-Datei auswählen +cbzToPDF.optimizeForEbook=PDF für E-Book-Reader optimieren (verwendet Ghostscript) + +#pdfToCBZ +pdfToCBZ.title=PDF zu CBZ +pdfToCBZ.header=PDF zu CBZ +pdfToCBZ.submit=Zu CBZ konvertieren +pdfToCBZ.selectText=PDF-Datei auswählen +pdfToCBZ.dpi=DPI (Punkte pro Zoll) + +#cbrToPDF +cbrToPDF.title=CBR zu PDF +cbrToPDF.header=CBR zu PDF +cbrToPDF.submit=Zu PDF konvertieren +cbrToPDF.selectText=CBR-Datei auswählen +cbrToPDF.optimizeForEbook=PDF für E-Book-Reader optimieren (verwendet Ghostscript) + +#pdfToCBR +pdfToCBR.title=PDF zu CBR +pdfToCBR.header=PDF zu CBR +pdfToCBR.submit=Zu CBR konvertieren +pdfToCBR.selectText=PDF-Datei auswählen +pdfToCBR.dpi=DPI (Punkte pro Zoll) +pdfToCBR.dpiHelp=Höhere DPI sorgen für bessere Qualität, erhöhen jedoch die Dateigröße. #pdfToImage pdfToImage.title=PDF zu Bild @@ -1435,10 +1488,11 @@ pdfToImage.colorType=Farbtyp pdfToImage.color=Farbe pdfToImage.grey=Graustufen pdfToImage.blackwhite=Schwarzweiß (Datenverlust möglich!) -pdfToImage.dpi=DPI (The server limit is {0} dpi) +pdfToImage.dpi=DPI (Das Serverlimit beträgt {0} dpi) pdfToImage.submit=Umwandeln pdfToImage.info=Python ist nicht installiert. Erforderlich für die WebP-Konvertierung. pdfToImage.placeholder=(z.B. 1,2,8 oder 4,7,12-16 oder 2n-1) +pdfToImage.includeAnnotations=Kommentare, Markierungen usw. einbeziehen #addPassword diff --git a/app/core/src/main/resources/messages_el_GR.properties b/app/core/src/main/resources/messages_el_GR.properties index 7f59f217e..ee9f4dde6 100644 --- a/app/core/src/main/resources/messages_el_GR.properties +++ b/app/core/src/main/resources/messages_el_GR.properties @@ -137,6 +137,7 @@ lang.yor=Yoruba addPageNumbers.fontSize=Μέγεθος γραμματοσειράς addPageNumbers.fontName=Όνομα γραμματοσειράς +addPageNumbers.fontColor=Font Colour pdfPrompt=Επιλέξτε PDF(s) multiPdfPrompt=Επιλέξτε PDFs (2+) multiPdfDropPrompt=Επιλέξτε (ή σύρετε & αφήστε) όλα τα PDF που χρειάζεστε @@ -193,6 +194,7 @@ error.fileFormatRequired=File must be in {0} format error.invalidFormat=Invalid {0} format: {1} error.endpointDisabled=This endpoint has been disabled by the admin error.urlNotReachable=URL is not reachable, please provide a valid URL +error.invalidUrlFormat=Invalid URL format provided. The provided format is invalid. # DPI and image rendering messages - used by frontend for dynamic translation # Backend sends: [TRANSLATE:messageKey:arg1,arg2] English message @@ -604,6 +606,22 @@ home.imageToPdf.title=Εικόνα σε PDF home.imageToPdf.desc=Μετατροπή εικόνας (PNG, JPEG, GIF) σε PDF. imageToPdf.tags=μετατροπή,εικόνα,jpg,φωτογραφία +home.cbzToPdf.title=CBZ to PDF +home.cbzToPdf.desc=Convert CBZ comic book archives to PDF format. +cbzToPdf.tags=conversion,comic,book,archive,cbz,zip + +home.cbrToPdf.title=CBR to PDF +home.cbrToPdf.desc=Convert CBR comic book archives to PDF format. +cbrToPdf.tags=conversion,comic,book,archive,cbr,rar + +home.pdfToCbz.title=PDF to CBZ +home.pdfToCbz.desc=Convert PDF files to CBZ comic book archives. +pdfToCbz.tags=conversion,comic,book,archive,cbz,pdf + +home.pdfToCbr.title=PDF to CBR +home.pdfToCbr.desc=Convert PDF files to CBR comic book archives. +pdfToCbr.tags=conversion,comic,book,archive,cbr,rar + home.pdfToImage.title=PDF σε εικόνα home.pdfToImage.desc=Μετατροπή PDF σε εικόνα. (PNG, JPEG, GIF) pdfToImage.tags=μετατροπή,εικόνα,jpg,φωτογραφία @@ -876,6 +894,12 @@ replace-color.selectText.8=Κίτρινο κείμενο σε μαύρο φόν replace-color.selectText.9=Πράσινο κείμενο σε μαύρο φόντο replace-color.selectText.10=Επιλογή χρώματος κειμένου replace-color.selectText.11=Επιλογή χρώματος φόντου +replace-color.selectText.12=Colour Space Conversion (CMYK for Printing) +replace-color.selectText.13=CMYK Colour Space Conversion +replace-color.selectText.14=This option converts the PDF from RGB colour space to CMYK colour space, which is optimized for professional printing. This process: +replace-color.selectText.15=Converts colours to CMYK (Cyan, Magenta, Yellow, Black) colour model used by professional printers +replace-color.selectText.16=Optimizes the PDF for print production with prepress settings +replace-color.selectText.17=May result in slight colour changes as CMYK has a smaller colour gamut than RGB replace-color.submit=Αντικατάσταση @@ -1081,7 +1105,7 @@ addPageNumbers.selectText.5=Σελίδες προς αρίθμηση addPageNumbers.selectText.6=Προσαρμοσμένο κείμενο addPageNumbers.customTextDesc=Προσαρμοσμένο κείμενο addPageNumbers.numberPagesDesc=Ποιες σελίδες να αριθμηθούν, προεπιλογή 'all', δέχεται επίσης 1-5 ή 2,5,9 κλπ -addPageNumbers.customNumberDesc=Προεπιλογή σε {n}, δέχεται επίσης 'Σελίδα {n} από {total}', 'Κείμενο-{n}', '{filename}-{n} +addPageNumbers.customNumberDesc=Προεπιλογή σε {n}, δέχεται επίσης 'Σελίδα {n} από {total}', 'Κείμενο-{n}', '{filename}-{n}' addPageNumbers.submit=Προσθήκη αριθμών σελίδων @@ -1217,6 +1241,7 @@ sign.previous=Προηγούμενη σελίδα sign.maintainRatio=Εναλλαγή διατήρησης αναλογίας διαστάσεων sign.undo=Undo sign.redo=Redo +sign.colour=Signature Colour #repair repair.title=Επιδιόρθωση @@ -1423,6 +1448,34 @@ imageToPDF.selectText.3=Λογική πολλαπλών αρχείων (Ενερ imageToPDF.selectText.4=Συγχώνευση σε ένα PDF imageToPDF.selectText.5=Μετατροπή σε ξεχωριστά PDF +#cbzToPDF +cbzToPDF.title=CBZ to PDF +cbzToPDF.header=CBZ to PDF +cbzToPDF.submit=Convert to PDF +cbzToPDF.selectText=Select CBZ file +cbzToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBZ +pdfToCBZ.title=PDF to CBZ +pdfToCBZ.header=PDF to CBZ +pdfToCBZ.submit=Convert to CBZ +pdfToCBZ.selectText=Select PDF file +pdfToCBZ.dpi=DPI (Dots Per Inch) + +#cbrToPDF +cbrToPDF.title=CBR to PDF +cbrToPDF.header=CBR to PDF +cbrToPDF.submit=Convert to PDF +cbrToPDF.selectText=Select CBR file +cbrToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBR +pdfToCBR.title=PDF to CBR +pdfToCBR.header=PDF to CBR +pdfToCBR.submit=Convert to CBR +pdfToCBR.selectText=Select PDF file +pdfToCBR.dpi=DPI (Dots Per Inch) +pdfToCBR.dpiHelp=Higher DPI results in better quality but larger file size. #pdfToImage pdfToImage.title=PDF σε εικόνα @@ -1439,6 +1492,7 @@ pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Μετατροπή pdfToImage.info=Η Python δεν είναι εγκατεστημένη. Απαιτείται για μετατροπή WebP. pdfToImage.placeholder=(π.χ. 1,2,8 ή 4,7,12-16 ή 2n-1) +pdfToImage.includeAnnotations=Include annotations (comments, highlights etc.) #addPassword diff --git a/app/core/src/main/resources/messages_en_GB.properties b/app/core/src/main/resources/messages_en_GB.properties index 99fb34b0d..5dcdf9d61 100644 --- a/app/core/src/main/resources/messages_en_GB.properties +++ b/app/core/src/main/resources/messages_en_GB.properties @@ -137,6 +137,7 @@ lang.yor=Yoruba addPageNumbers.fontSize=Font Size addPageNumbers.fontName=Font Name +addPageNumbers.fontColor=Font Colour pdfPrompt=Select PDF(s) multiPdfPrompt=Select PDFs (2+) multiPdfDropPrompt=Select (or drag & drop) all PDFs you require @@ -193,6 +194,7 @@ error.fileFormatRequired=File must be in {0} format error.invalidFormat=Invalid {0} format: {1} error.endpointDisabled=This endpoint has been disabled by the admin error.urlNotReachable=URL is not reachable, please provide a valid URL +error.invalidUrlFormat=Invalid URL format provided. The provided format is invalid. # DPI and image rendering messages - used by frontend for dynamic translation # Backend sends: [TRANSLATE:messageKey:arg1,arg2] English message @@ -604,6 +606,22 @@ home.imageToPdf.title=Image to PDF home.imageToPdf.desc=Convert a image (PNG, JPEG, GIF, PSD) to PDF. imageToPdf.tags=conversion,img,jpg,picture,photo,psd,photoshop +home.cbzToPdf.title=CBZ to PDF +home.cbzToPdf.desc=Convert CBZ comic book archives to PDF format. +cbzToPdf.tags=conversion,comic,book,archive,cbz,zip + +home.cbrToPdf.title=CBR to PDF +home.cbrToPdf.desc=Convert CBR comic book archives to PDF format. +cbrToPdf.tags=conversion,comic,book,archive,cbr,rar + +home.pdfToCbz.title=PDF to CBZ +home.pdfToCbz.desc=Convert PDF files to CBZ comic book archives. +pdfToCbz.tags=conversion,comic,book,archive,cbz,pdf + +home.pdfToCbr.title=PDF to CBR +home.pdfToCbr.desc=Convert PDF files to CBR comic book archives. +pdfToCbr.tags=conversion,comic,book,archive,cbr,rar + home.pdfToImage.title=PDF to Image home.pdfToImage.desc=Convert a PDF to a image. (PNG, JPEG, GIF, PSD) pdfToImage.tags=conversion,img,jpg,picture,photo,psd,photoshop @@ -876,6 +894,12 @@ replace-color.selectText.8=Yellow text on black background replace-color.selectText.9=Green text on black background replace-color.selectText.10=Choose text Colour replace-color.selectText.11=Choose background Colour +replace-color.selectText.12=Colour Space Conversion (CMYK for Printing) +replace-color.selectText.13=CMYK Colour Space Conversion +replace-color.selectText.14=This option converts the PDF from RGB colour space to CMYK colour space, which is optimized for professional printing. This process: +replace-color.selectText.15=Converts colours to CMYK (Cyan, Magenta, Yellow, Black) colour model used by professional printers +replace-color.selectText.16=Optimizes the PDF for print production with prepress settings +replace-color.selectText.17=May result in slight colour changes as CMYK has a smaller colour gamut than RGB replace-color.submit=Replace @@ -1081,7 +1105,7 @@ addPageNumbers.selectText.5=Pages to Number addPageNumbers.selectText.6=Custom Text addPageNumbers.customTextDesc=Custom Text addPageNumbers.numberPagesDesc=Which pages to number, default 'all', also accepts 1-5 or 2,5,9 etc -addPageNumbers.customNumberDesc=Defaults to {n}, also accepts 'Page {n} of {total}', 'Text-{n}', '{filename}-{n} +addPageNumbers.customNumberDesc=Defaults to {n}, also accepts 'Page {n} of {total}', 'Text-{n}', '{filename}-{n}' addPageNumbers.submit=Add Page Numbers @@ -1217,6 +1241,7 @@ sign.previous=Previous page sign.maintainRatio=Toggle maintain aspect ratio sign.undo=Undo sign.redo=Redo +sign.colour=Signature Colour #repair repair.title=Repair @@ -1423,6 +1448,34 @@ imageToPDF.selectText.3=Multi file logic (Only enabled if working with multiple imageToPDF.selectText.4=Merge into single PDF imageToPDF.selectText.5=Convert to separate PDFs +#cbzToPDF +cbzToPDF.title=CBZ to PDF +cbzToPDF.header=CBZ to PDF +cbzToPDF.submit=Convert to PDF +cbzToPDF.selectText=Select CBZ file +cbzToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBZ +pdfToCBZ.title=PDF to CBZ +pdfToCBZ.header=PDF to CBZ +pdfToCBZ.submit=Convert to CBZ +pdfToCBZ.selectText=Select PDF file +pdfToCBZ.dpi=DPI (Dots Per Inch) + +#cbrToPDF +cbrToPDF.title=CBR to PDF +cbrToPDF.header=CBR to PDF +cbrToPDF.submit=Convert to PDF +cbrToPDF.selectText=Select CBR file +cbrToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBR +pdfToCBR.title=PDF to CBR +pdfToCBR.header=PDF to CBR +pdfToCBR.submit=Convert to CBR +pdfToCBR.selectText=Select PDF file +pdfToCBR.dpi=DPI (Dots Per Inch) +pdfToCBR.dpiHelp=Higher DPI results in better quality but larger file size. #pdfToImage pdfToImage.title=PDF to Image @@ -1439,6 +1492,7 @@ pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Convert pdfToImage.info=Python is not installed. Required for WebP conversion. pdfToImage.placeholder=(e.g. 1,2,8 or 4,7,12-16 or 2n-1) +pdfToImage.includeAnnotations=Include annotations (comments, highlights etc.) #addPassword diff --git a/app/core/src/main/resources/messages_en_US.properties b/app/core/src/main/resources/messages_en_US.properties index 8ccbd7c99..e550e9c0d 100644 --- a/app/core/src/main/resources/messages_en_US.properties +++ b/app/core/src/main/resources/messages_en_US.properties @@ -137,6 +137,7 @@ lang.yor=Yoruba addPageNumbers.fontSize=Font Size addPageNumbers.fontName=Font Name +addPageNumbers.fontColor=Font Colour pdfPrompt=Select PDF(s) multiPdfPrompt=Select PDFs (2+) multiPdfDropPrompt=Select (or drag & drop) all PDFs you require @@ -193,6 +194,7 @@ error.fileFormatRequired=File must be in {0} format error.invalidFormat=Invalid {0} format: {1} error.endpointDisabled=This endpoint has been disabled by the admin error.urlNotReachable=URL is not reachable, please provide a valid URL +error.invalidUrlFormat=Invalid URL format provided. The provided format is invalid. # DPI and image rendering messages - used by frontend for dynamic translation # Backend sends: [TRANSLATE:messageKey:arg1,arg2] English message @@ -604,6 +606,22 @@ home.imageToPdf.title=Image to PDF home.imageToPdf.desc=Convert a image (PNG, JPEG, GIF, PSD) to PDF. imageToPdf.tags=conversion,img,jpg,picture,photo,psd,photoshop +home.cbzToPdf.title=CBZ to PDF +home.cbzToPdf.desc=Convert CBZ comic book archives to PDF format. +cbzToPdf.tags=conversion,comic,book,archive,cbz,zip + +home.cbrToPdf.title=CBR to PDF +home.cbrToPdf.desc=Convert CBR comic book archives to PDF format. +cbrToPdf.tags=conversion,comic,book,archive,cbr,rar + +home.pdfToCbz.title=PDF to CBZ +home.pdfToCbz.desc=Convert PDF files to CBZ comic book archives. +pdfToCbz.tags=conversion,comic,book,archive,cbz,pdf + +home.pdfToCbr.title=PDF to CBR +home.pdfToCbr.desc=Convert PDF files to CBR comic book archives. +pdfToCbr.tags=conversion,comic,book,archive,cbr,rar + home.pdfToImage.title=PDF to Image home.pdfToImage.desc=Convert a PDF to a image. (PNG, JPEG, GIF, PSD) pdfToImage.tags=conversion,img,jpg,picture,photo,psd,photoshop @@ -876,6 +894,12 @@ replace-color.selectText.8=Yellow text on black background replace-color.selectText.9=Green text on black background replace-color.selectText.10=Choose text Color replace-color.selectText.11=Choose background Color +replace-color.selectText.12=Colour Space Conversion (CMYK for Printing) +replace-color.selectText.13=CMYK Colour Space Conversion +replace-color.selectText.14=This option converts the PDF from RGB colour space to CMYK colour space, which is optimized for professional printing. This process: +replace-color.selectText.15=Converts colours to CMYK (Cyan, Magenta, Yellow, Black) colour model used by professional printers +replace-color.selectText.16=Optimizes the PDF for print production with prepress settings +replace-color.selectText.17=May result in slight colour changes as CMYK has a smaller colour gamut than RGB replace-color.submit=Replace @@ -1081,7 +1105,7 @@ addPageNumbers.selectText.5=Pages to Number addPageNumbers.selectText.6=Custom Text addPageNumbers.customTextDesc=Custom Text addPageNumbers.numberPagesDesc=Which pages to number, default 'all', also accepts 1-5 or 2,5,9 etc -addPageNumbers.customNumberDesc=Defaults to {n}, also accepts 'Page {n} of {total}', 'Text-{n}', '{filename}-{n} +addPageNumbers.customNumberDesc=Defaults to {n}, also accepts 'Page {n} of {total}', 'Text-{n}', '{filename}-{n}' addPageNumbers.submit=Add Page Numbers @@ -1217,6 +1241,7 @@ sign.previous=Previous page sign.maintainRatio=Toggle maintain aspect ratio sign.undo=Undo sign.redo=Redo +sign.colour=Signature Colour #repair repair.title=Repair @@ -1423,6 +1448,34 @@ imageToPDF.selectText.3=Multi file logic (Only enabled if working with multiple imageToPDF.selectText.4=Merge into single PDF imageToPDF.selectText.5=Convert to separate PDFs +#cbzToPDF +cbzToPDF.title=CBZ to PDF +cbzToPDF.header=CBZ to PDF +cbzToPDF.submit=Convert to PDF +cbzToPDF.selectText=Select CBZ file +cbzToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBZ +pdfToCBZ.title=PDF to CBZ +pdfToCBZ.header=PDF to CBZ +pdfToCBZ.submit=Convert to CBZ +pdfToCBZ.selectText=Select PDF file +pdfToCBZ.dpi=DPI (Dots Per Inch) + +#cbrToPDF +cbrToPDF.title=CBR to PDF +cbrToPDF.header=CBR to PDF +cbrToPDF.submit=Convert to PDF +cbrToPDF.selectText=Select CBR file +cbrToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBR +pdfToCBR.title=PDF to CBR +pdfToCBR.header=PDF to CBR +pdfToCBR.submit=Convert to CBR +pdfToCBR.selectText=Select PDF file +pdfToCBR.dpi=DPI (Dots Per Inch) +pdfToCBR.dpiHelp=Higher DPI results in better quality but larger file size. #pdfToImage pdfToImage.title=PDF to Image @@ -1439,6 +1492,7 @@ pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Convert pdfToImage.info=Python is not installed. Required for WebP conversion. pdfToImage.placeholder=(e.g. 1,2,8 or 4,7,12-16 or 2n-1) +pdfToImage.includeAnnotations=Include annotations (comments, highlights etc.) #addPassword diff --git a/app/core/src/main/resources/messages_es_ES.properties b/app/core/src/main/resources/messages_es_ES.properties index ae63d5107..b9f6d19f0 100644 --- a/app/core/src/main/resources/messages_es_ES.properties +++ b/app/core/src/main/resources/messages_es_ES.properties @@ -5,138 +5,139 @@ language.direction=ltr # Language names for reuse throughout the application -lang.afr=Afrikaans -lang.amh=Amharic -lang.ara=Arabic -lang.asm=Assamese -lang.aze=Azerbaijani -lang.aze_cyrl=Azerbaijani (Cyrillic) -lang.bel=Belarusian -lang.ben=Bengali -lang.bod=Tibetan -lang.bos=Bosnian -lang.bre=Breton -lang.bul=Bulgarian -lang.cat=Catalan +lang.afr=Afrikáans +lang.amh=Amárico +lang.ara=Árabe +lang.asm=Asamés +lang.aze=Azerí +lang.aze_cyrl=Azerí (Cirílico) +lang.bel=Bielorruso +lang.ben=Bengalí +lang.bod=Tibetano +lang.bos=Bosnio +lang.bre=Bretón +lang.bul=Búlgaro +lang.cat=Catalán lang.ceb=Cebuano -lang.ces=Czech -lang.chi_sim=Chinese (Simplified) -lang.chi_sim_vert=Chinese (Simplified, Vertical) -lang.chi_tra=Chinese (Traditional) -lang.chi_tra_vert=Chinese (Traditional, Vertical) +lang.ces=Checo +lang.chi_sim=Chino (Simplificado) +lang.chi_sim_vert=Chino (Simplificado, Vertical) +lang.chi_tra=Chino (Tradicional) +lang.chi_tra_vert=Chino (Tradicional, Vertical) lang.chr=Cherokee -lang.cos=Corsican -lang.cym=Welsh -lang.dan=Danish -lang.dan_frak=Danish (Fraktur) -lang.deu=German -lang.deu_frak=German (Fraktur) +lang.cos=Corso +lang.cym=Galés +lang.dan=Danés +lang.dan_frak=Danés (Fraktur) +lang.deu=Alemán +lang.deu_frak=Alemán (Fraktur) lang.div=Divehi lang.dzo=Dzongkha -lang.ell=Greek -lang.eng=English -lang.enm=English, Middle (1100-1500) +lang.ell=Griego +lang.eng=Inglés +lang.enm=Inglés Medio (1100–1500) lang.epo=Esperanto -lang.equ=Math / equation detection module -lang.est=Estonian -lang.eus=Basque -lang.fao=Faroese -lang.fas=Persian +lang.equ=Matematicas / módulo de detección de ecuaciones +lang.est=Estonio +lang.eus=Euskera +lang.fao=Feroés +lang.fas=Persa lang.fil=Filipino -lang.fin=Finnish -lang.fra=French -lang.frk=Frankish -lang.frm=French, Middle (ca.1400-1600) -lang.fry=Western Frisian -lang.gla=Scottish Gaelic -lang.gle=Irish -lang.glg=Galician -lang.grc=Ancient Greek +lang.fin=Finlandés +lang.fra=Francés +lang.frk=Francón +lang.frm=Francés Medio (ca.1400-1600) +lang.fry=Frisón Occidental +lang.gla=Gaélico Escocés +lang.gle=Irlandés +lang.glg=Gallego +lang.grc=Griego Antiguo lang.guj=Gujarati -lang.hat=Haitian, Haitian Creole -lang.heb=Hebrew +lang.hat=Haitiano, Criollo Haitiano +lang.heb=Hebreo lang.hin=Hindi -lang.hrv=Croatian -lang.hun=Hungarian -lang.hye=Armenian +lang.hrv=Croata +lang.hun=Húngaro +lang.hye=Armenio lang.iku=Inuktitut -lang.ind=Indonesian -lang.isl=Icelandic -lang.ita=Italian -lang.ita_old=Italian (Old) -lang.jav=Javanese -lang.jpn=Japanese -lang.jpn_vert=Japanese (Vertical) +lang.ind=Indonesio +lang.isl=Islandés +lang.ita=Italiano +lang.ita_old=Italiano (Antiguo) +lang.jav=Javanés +lang.jpn=Japonés +lang.jpn_vert=Japonés (Vertical) lang.kan=Kannada -lang.kat=Georgian -lang.kat_old=Georgian (Old) -lang.kaz=Kazakh -lang.khm=Central Khmer -lang.kir=Kirghiz, Kyrgyz -lang.kmr=Northern Kurdish -lang.kor=Korean -lang.kor_vert=Korean (Vertical) +lang.kat=Georgiano +lang.kat_old=Georgiano (Antiguo) +lang.kaz=Kazajo +lang.khm=Jemer Central +lang.kir=Kirguís +lang.kmr=Kurdo del Norte +lang.kor=Coreano +lang.kor_vert=Coreano (Vertical) lang.lao=Lao -lang.lat=Latin -lang.lav=Latvian -lang.lit=Lithuanian -lang.ltz=Luxembourgish +lang.lat=Latín +lang.lav=Letón +lang.lit=Lituano +lang.ltz=Luxemburgués lang.mal=Malayalam -lang.mar=Marathi -lang.mkd=Macedonian -lang.mlt=Maltese -lang.mon=Mongolian -lang.mri=Maori -lang.msa=Malay -lang.mya=Burmese -lang.nep=Nepali -lang.nld=Dutch; Flemish -lang.nor=Norwegian -lang.oci=Occitan (post 1500) +lang.mar=Maratí +lang.mkd=Macedonio +lang.mlt=Maltés +lang.mon=Mongol +lang.mri=Maorí +lang.msa=Malayo +lang.mya=Birmano +lang.nep=Nepalí +lang.nld=Neerlandés; Flamenco +lang.nor=Noruego +lang.oci=Occitano (posterior a 1500) lang.ori=Oriya -lang.osd=Orientation and script detection module -lang.pan=Panjabi, Punjabi -lang.pol=Polish -lang.por=Portuguese -lang.pus=Pushto, Pashto +lang.osd=Módulo de detección de orientación y escritura +lang.pan=Panyabí, Punjabi +lang.pol=Polaco +lang.por=Portugués +lang.pus=Pastún lang.que=Quechua -lang.ron=Romanian, Moldavian, Moldovan -lang.rus=Russian -lang.san=Sanskrit -lang.sin=Sinhala, Sinhalese -lang.slk=Slovak -lang.slk_frak=Slovak (Fraktur) -lang.slv=Slovenian +lang.ron=Rumano, Moldavo +lang.rus=Ruso +lang.san=Sánscrito +lang.sin=Cingalés +lang.slk=Eslovaco +lang.slk_frak=Eslovaco (Fraktur) +lang.slv=Esloveno lang.snd=Sindhi -lang.spa=Spanish -lang.spa_old=Spanish (Old) -lang.sqi=Albanian -lang.srp=Serbian -lang.srp_latn=Serbian (Latin) -lang.sun=Sundanese -lang.swa=Swahili -lang.swe=Swedish -lang.syr=Syriac +lang.spa=Español +lang.spa_old=Español (Antiguo) +lang.sqi=Albanés +lang.srp=Serbio +lang.srp_latn=Serbio (Latino) +lang.sun=Sundanés +lang.swa=Suajili +lang.swe=Sueco +lang.syr=Siríaco lang.tam=Tamil -lang.tat=Tatar +lang.tat=Tártaro lang.tel=Telugu -lang.tgk=Tajik -lang.tgl=Tagalog -lang.tha=Thai -lang.tir=Tigrinya -lang.ton=Tonga (Tonga Islands) -lang.tur=Turkish -lang.uig=Uighur, Uyghur -lang.ukr=Ukrainian +lang.tgk=Tayiko +lang.tgl=Tagalo +lang.tha=Tailandés +lang.tir=Tigriña +lang.ton=Tonga (Islas Tonga) +lang.tur=Turco +lang.uig=Uigur +lang.ukr=Ucraniano lang.urd=Urdu -lang.uzb=Uzbek -lang.uzb_cyrl=Uzbek (Cyrillic) -lang.vie=Vietnamese -lang.yid=Yiddish +lang.uzb=Uzbeko +lang.uzb_cyrl=Uzbeko (Cirílico) +lang.vie=Vietnamita +lang.yid=Yidis lang.yor=Yoruba addPageNumbers.fontSize=Tamaño de Letra addPageNumbers.fontName=Nombre de Letra +addPageNumbers.fontColor=Font Colour pdfPrompt=Seleccionar PDF(s) multiPdfPrompt=Seleccionar PDFs (2+) multiPdfDropPrompt=Seleccione (o arrastre y suelte) todos los PDFs que quiera @@ -170,67 +171,68 @@ sizes.medium=Mediano sizes.large=Grande sizes.x-large=Extra grande error.pdfPassword=El documento PDF está protegido con contraseña y no se ha proporcionado o es incorrecta -error.pdfCorrupted=PDF file appears to be corrupted or damaged. Please try using the 'Repair PDF' feature first to fix the file before proceeding with this operation. -error.pdfCorruptedMultiple=One or more PDF files appear to be corrupted or damaged. Please try using the 'Repair PDF' feature on each file first before attempting to merge them. -error.pdfCorruptedDuring=Error {0}: PDF file appears to be corrupted or damaged. Please try using the 'Repair PDF' feature first to fix the file before proceeding with this operation. +error.pdfCorrupted=El documento PDF parece estar corrupto o dañado. Intente usar la función 'Reparar PDF' para arreglar el archivo antes de continuar con esta operación. +error.pdfCorruptedMultiple=Uno o varios documentos PDF parecen estar corruptos o dañados. Intente usar la función 'Reparar PDF' con cada archivo antes de intentar unirlos. +error.pdfCorruptedDuring=Error {0}: el documento PDF parece estar corrupto o dañado. Intente usar la función 'Reparar PDF' para arreglar el archivo antes de continuar con esta operación. # Frontend corruption error messages -error.pdfInvalid=The PDF file "{0}" appears to be corrupted or has an invalid structure. Please try using the 'Repair PDF' feature to fix the file before proceeding. -error.tryRepair=Try using the Repair PDF feature to fix corrupted files. +error.pdfInvalid=El documento PDF "{0}" parece estar corrupto o tiene una estructura no válida. Intente usar la función 'Reparar PDF' para arreglar el archivo antes de continuar. +error.tryRepair=Pruebe a usar 'Reparar PDF' para arreglar los documentos corruptos. # Additional error messages -error.pdfEncryption=The PDF appears to have corrupted encryption data. This can happen when the PDF was created with incompatible encryption methods. Please try using the 'Repair PDF' feature first, or contact the document creator for a new copy. -error.fileProcessing=An error occurred while processing the file during {0} operation: {1} +error.pdfEncryption=El PDF parece tener datos cifrados corruptos. Esto puede ocurrir cuando el PDF se creó con métodos de cifrado incompatibles. Intente usar la función 'Reparar PDF', o contacte con el creador del documento para obtener una nueva copia. +error.fileProcessing=Se ha producido un error al procesar el archivo durante la operación {0}: {1} # Generic error message templates -error.toolNotInstalled={0} is not installed -error.toolRequired={0} is required for {1} -error.conversionFailed={0} conversion failed -error.commandFailed={0} command failed -error.algorithmNotAvailable={0} algorithm not available -error.optionsNotSpecified={0} options are not specified -error.fileFormatRequired=File must be in {0} format -error.invalidFormat=Invalid {0} format: {1} -error.endpointDisabled=This endpoint has been disabled by the admin -error.urlNotReachable=URL is not reachable, please provide a valid URL +error.toolNotInstalled={0} no está instalado +error.toolRequired={0} es necesario para {1} +error.conversionFailed=La conversión {0} ha fallado +error.commandFailed=La orden {0} ha fallado +error.algorithmNotAvailable=El algoritmo {0} no está disponible +error.optionsNotSpecified=No se han indicado las opciones {0} +error.fileFormatRequired=El archivo debe estar en formato {0} +error.invalidFormat=Formato {0} no válido: {1} +error.endpointDisabled=Esta función ha sido desactivada por el administrador +error.urlNotReachable=La URL no está accesible. Proporcione una URL válida +error.invalidUrlFormat=Invalid URL format provided. The provided format is invalid. # DPI and image rendering messages - used by frontend for dynamic translation # Backend sends: [TRANSLATE:messageKey:arg1,arg2] English message # Frontend parses this and replaces with localized versions using these keys -error.dpiExceedsLimit=DPI value {0} exceeds maximum safe limit of {1}. High DPI values can cause memory issues and crashes. Please use a lower DPI value. -error.pageTooBigForDpi=PDF page {0} is too large to render at {1} DPI. Please try a lower DPI value (recommended: 150 or less). -error.pageTooBigExceedsArray=PDF page {0} is too large to render at {1} DPI. The resulting image would exceed Java's maximum array size. Please try a lower DPI value (recommended: 150 or less). -error.pageTooBigFor300Dpi=PDF page {0} is too large to render at 300 DPI. The resulting image would exceed Java's maximum array size. Please use a lower DPI value for PDF-to-image conversion. +error.dpiExceedsLimit=El valor DPI {0} supera el máximo de {1} considerado seguro. Valores altos de DPI pueden provocar problemas de memoria y caidas de la aplicación. Reduzca el valor de DPI. +error.pageTooBigForDpi=La página {0} del PDF es demasiado grande para ser generada a {1} DPI. Pruebe con un valor de DPI menor (recomendado: 150 o menos). +error.pageTooBigExceedsArray=La página {0} del PDF es demasiado grande para ser generada a {1} DPI. La imagen resultante superaría el tamaño máximo de Java. Pruebe con un valor de DPI menor (recomendado: 150 o menos). +error.pageTooBigFor300Dpi=La página {0} del PDF es demasiado grande para ser generada a 300 DPI. La imagen resultante superaría el tamaño máximo de Java. Use un valor de DPI menor para la conversión de PDF a imagen. # URL and website conversion messages # System requirements messages # Authentication and security messages -error.apiKeyInvalid=API key is not valid. -error.userNotFound=User not found. -error.passwordRequired=Password must not be null. -error.accountLocked=Your account has been locked due to too many failed login attempts. -error.invalidEmail=Invalid email addresses provided. -error.emailAttachmentRequired=An attachment is required to send the email. -error.signatureNotFound=Signature file not found. +error.apiKeyInvalid=La clave API no es válida. +error.userNotFound=Usuario no encontrado. +error.passwordRequired=La contraseña no puede estar en blanco. +error.accountLocked=Su cuenta ha sido bloqueada por exceso de intentos de inicio de sesión fallidos. +error.invalidEmail=Se ha proporcionado una dirección de correo electrónico no válida. +error.emailAttachmentRequired=Es necesario un adjunto para enviar el correo. +error.signatureNotFound=Archivo de firma no encontrado. # File processing messages -error.fileNotFound=File not found with ID: {0} +error.fileNotFound=No se ha encontrado un archivo con ID: {0} # Database and configuration messages -error.noBackupScripts=No backup scripts were found. -error.unsupportedProvider={0} is not currently supported. -error.pathTraversalDetected=Path traversal detected for security reasons. +error.noBackupScripts=No se han encontrado scripts de copias de seguridad. +error.unsupportedProvider={0} no está disponible actualmente. +error.pathTraversalDetected=Se detectó un recorrido de ruta por razones de seguridad. # Validation messages -error.invalidArgument=Invalid argument: {0} -error.argumentRequired={0} must not be null -error.operationFailed=Operation failed: {0} -error.angleNotMultipleOf90=Angle must be a multiple of 90 +error.invalidArgument=Argumento no válido: {0} +error.argumentRequired={0} no puede estar vacío +error.operationFailed=La operación ha fallado: {0} +error.angleNotMultipleOf90=El ángulo debe ser múltiplo de 90 error.pdfBookmarksNotFound=No PDF bookmarks/outline found in document -error.fontLoadingFailed=Error processing font file -error.fontDirectoryReadFailed=Failed to read font directory +error.fontLoadingFailed=Error al procesar el archivo de fuente +error.fontDirectoryReadFailed=Error al leer el directorio de fuentes delete=Borrar username=Nombre de usuario password=Contraseña @@ -258,9 +260,9 @@ deleteUsernameExistsMessage=El usuario no existe y no puede eliminarse. downgradeCurrentUserMessage=No se puede degradar el rol del usuario actual disabledCurrentUserMessage=El usuario actual no se puede deshabilitar downgradeCurrentUserLongMessage=No se puede degradar el rol del usuario actual. Por lo tanto, el usuario actual no se mostrará. -userAlreadyExistsOAuthMessage=La usuario ya existe como usuario de OAuth2. +userAlreadyExistsOAuthMessage=El usuario ya existe como usuario de OAuth2. userAlreadyExistsWebMessage=El usuario ya existe como usuario web. -invalidRoleMessage=Invalid role. +invalidRoleMessage=Rol no valido. error=Error oops=¡Ups! help=Ayuda @@ -273,7 +275,7 @@ color=Color sponsor=Patrocinador info=Información pro=Pro -proFeatures=Pro Features +proFeatures=Funciones Pro page=Página pages=Páginas loading=Cargando... @@ -281,12 +283,12 @@ addToDoc=Agregar al Documento reset=Restablecer apply=Aplicar noFileSelected=No ha seleccionado ningún archivo. Por favor, cargue uno. -view=View -cancel=Cancel +view=Ver +cancel=Cancelar -back.toSettings=Back to Settings -back.toHome=Back to Home -back.toAdmin=Back to Admin +back.toSettings=Volver a Configuración +back.toHome=Volver a Inicio +back.toAdmin=Volver a Administración legal.privacy=Política de Privacidad legal.terms=Términos y Condiciones @@ -327,7 +329,7 @@ enterpriseEdition.button=Actualiza a Pro enterpriseEdition.warning=Esta característica está únicamente disponible para usuarios Pro. enterpriseEdition.yamlAdvert=Stirling PDF Pro soporta configuración de ficheros YAML y otras características SSO. enterpriseEdition.ssoAdvert=¿Busca más funciones de administración de usuarios? Consulte Stirling PDF Pro -enterpriseEdition.proTeamFeatureDisabled=Team management features require a Pro licence or higher +enterpriseEdition.proTeamFeatureDisabled=Las funciones de gestión de equipos necesitan de una licencia Pro o superior ################# @@ -368,36 +370,36 @@ settings.update=Actualización disponible settings.updateAvailable={0} es la versión instalada. Hay disponible una versión nueva ({1}). # Update modal and notification strings -update.urgentUpdateAvailable=🚨 Update Available -update.updateAvailable=Update Available -update.modalTitle=Update Available -update.current=Current -update.latest=Latest -update.latestStable=Latest Stable -update.priority=Priority -update.recommendedAction=Recommended Action +update.urgentUpdateAvailable=🚨 Actualización disponible +update.updateAvailable=Actualización disponible +update.modalTitle=Actualización disponible +update.current=Actual +update.latest=Última +update.latestStable=Última estable +update.priority=Prioridad +update.recommendedAction=Acción recomendada update.breakingChangesDetected=⚠️ Breaking Changes Detected -update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. -update.migrationGuides=Migration Guides: -update.viewGuide=View Guide -update.loadingDetailedInfo=Loading detailed version information... -update.close=Close -update.viewAllReleases=View All Releases -update.downloadLatest=Download Latest -update.availableUpdates=Available Updates: -update.unableToLoadDetails=Unable to load detailed version information. -update.version=Version +update.breakingChangesMessage=Esta actualización contiene cambios importantes. Revise las guías de migración más abajo. +update.migrationGuides=Guías de migración: +update.viewGuide=Ver guía +update.loadingDetailedInfo=Cargando información de versión detallada... +update.close=Cerrar +update.viewAllReleases=Ver todas las versiones +update.downloadLatest=Descargar la última versión +update.availableUpdates=Actualizaciones disponibles: +update.unableToLoadDetails=No ha sido posible descargar la información de versión detallada. +update.version=Versión # Update priority levels -update.priority.urgent=URGENT +update.priority.urgent=URGENTE update.priority.normal=NORMAL -update.priority.minor=MINOR -update.priority.low=LOW +update.priority.minor=MENOR +update.priority.low=BAJA # Breaking changes text -update.breakingChanges=Breaking Changes: -update.breakingChangesDefault=This version contains breaking changes -update.migrationGuide=Migration Guide +update.breakingChanges=Cambios importantes: +update.breakingChangesDefault=Esta versión contiene cambios importantes +update.migrationGuide=Guia de migración settings.appVersion=Versión de la aplicación: settings.downloadOption.title=Elegir la opción de descarga (para descargas de un solo archivo sin ZIP): settings.downloadOption.1=Abrir en la misma ventana @@ -423,15 +425,15 @@ changeCreds.submit=Enviar cambios account.title=Configuración de la cuenta account.accountSettings=Configuración de la cuenta -account.adminSettings=Configuración de Administrador - Ver y Añadir Usuarios +account.adminSettings=Configuración de Administrador - Ver y añadir usuarios account.userControlSettings=Configuración de control de usuario account.changeUsername=Cambiar nombre de usuario account.newUsername=nuevo nombre de usuario account.password=Confirmar contraseña account.oldPassword=Contraseña anterior -account.newPassword=Nueva Contraseña -account.changePassword=Cambiar Contraseña -account.confirmNewPassword=Confirmar Nueva Contraseña +account.newPassword=Nueva contraseña +account.changePassword=Cambiar contraseña +account.confirmNewPassword=Confirmar nueva contraseña account.signOut=Cerrar sesión account.yourApiKey=Su clave API account.syncTitle=Sincronizar la configuración del navegador con la cuenta @@ -440,8 +442,8 @@ account.property=Propiedad account.webBrowserSettings=Configuración del navegador account.syncToBrowser=Sincronizar cuenta -> Navegador account.syncToAccount=Sincronizar cuenta <- Navegador -account.adminTitle=Administrator Tools -account.adminNotif=You have admin privileges. Access system settings and user management. +account.adminTitle=Herramientas de administración +account.adminNotif=Tiene privilegios de administrador. Acceda a la configuración del sistema y a la gestión de usuarios. adminUserSettings.title=Configuración de control de usuario @@ -472,48 +474,48 @@ adminUserSettings.disabledUsers=Usuarios deshabilitados: adminUserSettings.totalUsers=Usuarios totales: adminUserSettings.lastRequest=Última petición adminUserSettings.usage=Ver uso -adminUserSettings.teams=View/Edit Teams -adminUserSettings.team=Team -adminUserSettings.manageTeams=Manage Teams -adminUserSettings.createTeam=Create Team -adminUserSettings.viewTeam=View Team -adminUserSettings.deleteTeam=Delete Team -adminUserSettings.teamName=Team Name -adminUserSettings.teamExists=Team already exists -adminUserSettings.teamCreated=Team created successfully -adminUserSettings.teamChanged=User's team was updated -adminUserSettings.teamHidden=Hidden -adminUserSettings.totalMembers=Total Members -adminUserSettings.confirmDeleteTeam=Are you sure you want to delete this team? +adminUserSettings.teams=Ver/Editar equpos +adminUserSettings.team=Equipo +adminUserSettings.manageTeams=Gestionar equipos +adminUserSettings.createTeam=Crear equipo +adminUserSettings.viewTeam=Ver equipo +adminUserSettings.deleteTeam=Eliminar equipo +adminUserSettings.teamName=Nombre del equipo +adminUserSettings.teamExists=El equipo ya existe +adminUserSettings.teamCreated=Equipo creado correctamente +adminUserSettings.teamChanged=Se ha actualizado el equipo del usuario +adminUserSettings.teamHidden=Oculto +adminUserSettings.totalMembers=Miembros totales +adminUserSettings.confirmDeleteTeam=¿Está seguro de querer eliminar este equipo? -teamCreated=Team created successfully -teamExists=A team with that name already exists -teamNameExists=Another team with that name already exists -teamNotFound=Team not found -teamDeleted=Team deleted -teamHasUsers=Cannot delete a team with users assigned -teamRenamed=Team renamed successfully +teamCreated=Equipo creado correctamente +teamExists=Ya existe un equipo con ese nombre +teamNameExists=Ya existe otro equipo con ese nombre +teamNotFound=Equipo no encontrado +teamDeleted=Equipo eliminado +teamHasUsers=No se puede eliminar un equipo con usuarios asignados +teamRenamed=Equipo renombrado correctamente # Team user management -team.addUser=Add User to Team -team.selectUser=Select User -team.warning.moveUser=Warning: This will move the user from "{0}" team to "{1}" team. Are you sure? -team.confirm.moveUser=Are you sure you want to move this user from "{0}" team to "{1}" team? -team.userAdded=User successfully added to team -team.back=Back to Teams -team.internal=Internal Team -team.internalTeamNotAccessible=The Internal team is a system team and cannot be accessed -team.cannotMoveInternalUsers=Users in the Internal team cannot be moved to other teams -team.hidden=Hidden -team.name=Team Name -team.totalMembers=Total Members -team.members=Members -team.username=Username -team.role=Role -team.status=Status -team.enabled=Enabled -team.disabled=Disabled -team.noMembers=This team has no members yet. +team.addUser=Añadir usuario al equipo +team.selectUser=Seleccionar usuario +team.warning.moveUser=Aviso: esto moverá el usuario del equipo "{0}" al equipo "{1}". ¿Está seguro? +team.confirm.moveUser=¿Está seguro de querer mover este usuario del equipo "{0}" al equpo "{1}"? +team.userAdded=Usuario añadido correctamente al equipo +team.back=Volver a Equipos +team.internal=Equipo interno +team.internalTeamNotAccessible=El equipo Interno es un equipo del sistema y no está accesible +team.cannotMoveInternalUsers=Lo usuarios del equipo interno no se pueden mover a otros equpos +team.hidden=Oculto +team.name=Nombre del equipo +team.totalMembers=Número total de miembros +team.members=Miembros +team.username=Nombre de usuario +team.role=Rol +team.status=Estado +team.enabled=Activo +team.disabled=desactivado +team.noMembers=Este equipo no tiene miembros todavía. @@ -604,6 +606,22 @@ home.imageToPdf.title=Imagen a PDF home.imageToPdf.desc=Convertir una imagen (PNG, JPEG, GIF) a PDF imageToPdf.tags=conversión,img,jpg,imagen,fotografía +home.cbzToPdf.title=CBZ to PDF +home.cbzToPdf.desc=Convert CBZ comic book archives to PDF format. +cbzToPdf.tags=conversion,comic,book,archive,cbz,zip + +home.cbrToPdf.title=CBR to PDF +home.cbrToPdf.desc=Convert CBR comic book archives to PDF format. +cbrToPdf.tags=conversion,comic,book,archive,cbr,rar + +home.pdfToCbz.title=PDF to CBZ +home.pdfToCbz.desc=Convert PDF files to CBZ comic book archives. +pdfToCbz.tags=conversion,comic,book,archive,cbz,pdf + +home.pdfToCbr.title=PDF to CBR +home.pdfToCbr.desc=Convert PDF files to CBR comic book archives. +pdfToCbr.tags=conversion,comic,book,archive,cbr,rar + home.pdfToImage.title=PDF a Imagen home.pdfToImage.desc=Convertir un PDF a una imagen (PNG, JPEG, GIF) pdfToImage.tags=conversión,img,jpg,imagen,fotografía @@ -772,21 +790,21 @@ home.HTMLToPDF.desc=Convierte cualquier archivo HTML o ZIP a PDF HTMLToPDF.tags=margen,contenido web,transformación,convertir #eml-to-pdf -home.EMLToPDF.title=Email to PDF -home.EMLToPDF.desc=Converts email (EML) files to PDF format including headers, body, and inline images -EMLToPDF.tags=email,conversion,eml,message,transformation,convert,mail +home.EMLToPDF.title=Correo a PDF +home.EMLToPDF.desc=Convierte archivos de correo (EML) a formato PDF, incluyendo cabeceras, cuerpo e imágenes en línea +EMLToPDF.tags=correo,conversión,eml,mensaje,transformación,conversión,mail -EMLToPDF.title=Email To PDF -EMLToPDF.header=Email To PDF -EMLToPDF.submit=Convert -EMLToPDF.downloadHtml=Download HTML intermediate file instead of PDF -EMLToPDF.downloadHtmlHelp=This allows you to see the HTML version before PDF conversion and can help debug formatting issues -EMLToPDF.includeAttachments=Include attachments in PDF -EMLToPDF.maxAttachmentSize=Maximum attachment size (MB) -EMLToPDF.help=Converts email (EML) files to PDF format including headers, body, and inline images -EMLToPDF.troubleshootingTip1=Email to HTML is a more reliable process, so with batch-processing it is recommended to save both -EMLToPDF.troubleshootingTip2=With a small number of Emails, if the PDF is malformed, you can download HTML and override some of the problematic HTML/CSS code. -EMLToPDF.troubleshootingTip3=Embeddings, however, do not work with HTMLs +EMLToPDF.title=Correo a PDF +EMLToPDF.header=Correo a PDF +EMLToPDF.submit=Convertir +EMLToPDF.downloadHtml=Descargar archivo HTML intermedio en lugar del PDF +EMLToPDF.downloadHtmlHelp=Este le permite ver una versión del HTML antes de la conversión del PDF y puede ayudar a depurar problemas de formato +EMLToPDF.includeAttachments=Incluir adjuntos en el PDF +EMLToPDF.maxAttachmentSize=Tamaño máximo del adjunto (MB) +EMLToPDF.help=Convierte archivos de correos electrónicos (EML) a formato PDF incluyendo cabeceras, cuerpo e imágenes en línea +EMLToPDF.troubleshootingTip1=EmailCorreo a HTML es un proceso más fiable, por lo que para el procesado por lotes se recomienda guardar ambos +EMLToPDF.troubleshootingTip2=Con un número pequeño de correos, si el PDF tiene un formato incorrecto, puede descargar el HTML y sobrescribir el código HTML/CSS problemático. +EMLToPDF.troubleshootingTip3=Las incrustaciones, sin embargo, no funcionan con HTML. home.MarkdownToPDF.title=Markdown a PDF home.MarkdownToPDF.desc=Convierte cualquier archivo Markdown a PDF @@ -815,13 +833,13 @@ home.showJS.title=Mostrar Javascript home.showJS.desc=Busca y muestra cualquier JS contenido en un PDF showJS.tags=JS -home.autoRedact.title=Auto Redactar -home.autoRedact.desc=Redactar automáticamente (ocultar) texto en un PDF según el texto introducido -autoRedact.tags=Redactar,Ocultar,ocultar,negro,subrayador,oculto +home.autoRedact.title=Auto censura +home.autoRedact.desc=Censurar automáticamente (ocultar) texto en un PDF según el texto introducido +autoRedact.tags=Censurar,Ocultar,ocultar,negro,subrayador,oculto -home.redact.title=Redacción Manual -home.redact.desc=Redacta un PDF basado en el texto seleccionado, dibuja formas y/o página(s) selecionada(s) -redact.tags=Redactar,Ocultar,oscurece,negro,marcador,oculto,manual +home.redact.title=Censura manual +home.redact.desc=Censura un PDF basado en el texto seleccionado, dibuja formas y/o página(s) selecionada(s) +redact.tags=Censurar,Ocultar,oscurece,negro,marcador,oculto,manual home.tableExtraxt.title=PDF a CSV home.tableExtraxt.desc=Extraer Tablas de un PDF convirtiéndolas a CSV @@ -876,6 +894,12 @@ replace-color.selectText.8=Texto amarillo sobre fondo negro replace-color.selectText.9=Texto verde sobre fondo negro replace-color.selectText.10=Elegir Color de Texto replace-color.selectText.11=Elegir Color de Fondo +replace-color.selectText.12=Colour Space Conversion (CMYK for Printing) +replace-color.selectText.13=CMYK Colour Space Conversion +replace-color.selectText.14=This option converts the PDF from RGB colour space to CMYK colour space, which is optimized for professional printing. This process: +replace-color.selectText.15=Converts colours to CMYK (Cyan, Magenta, Yellow, Black) colour model used by professional printers +replace-color.selectText.16=Optimizes the PDF for print production with prepress settings +replace-color.selectText.17=May result in slight colour changes as CMYK has a smaller colour gamut than RGB replace-color.submit=Reemplazar @@ -911,10 +935,10 @@ login.logoutMessage=You have been logged out. login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator. #auto-redact -autoRedact.title=Auto Censurar Texto -autoRedact.header=Auto Censurar Texto +autoRedact.title=Auto censurar texto +autoRedact.header=Auto censurar texto autoRedact.colorLabel=Color -autoRedact.textsToRedactLabel=Texto para Censurar (separado por líneas) +autoRedact.textsToRedactLabel=Texto a censurar (separado por líneas) autoRedact.textsToRedactPlaceholder=por ej. \nConfidencial \nAlto-Secreto autoRedact.useRegexLabel=Usar Regex autoRedact.wholeWordSearchLabel=Búsqueda por palabra completa @@ -974,28 +998,28 @@ getPdfInfo.title=Obtener Información del PDF getPdfInfo.header=Obtener Información del PDF getPdfInfo.submit=Obtener Información getPdfInfo.downloadJson=Descargar JSON -getPdfInfo.summary=PDF Summary -getPdfInfo.summary.encrypted=This PDF is encrypted so may face issues with some applications +getPdfInfo.summary=Resumen del PDF +getPdfInfo.summary.encrypted=Este PDF está cifrado, por lo que puede dar problemas con algunas aplicaciones getPdfInfo.summary.permissions=This PDF has {0} restricted permissions which may limit what you can do with it -getPdfInfo.summary.compliance=This PDF complies with the {0} standard -getPdfInfo.summary.basicInfo=Basic Information -getPdfInfo.summary.docInfo=Document Information -getPdfInfo.summary.encrypted.alert=Encrypted PDF - This document is password protected -getPdfInfo.summary.not.encrypted.alert=Unencrypted PDF - No password protection -getPdfInfo.summary.permissions.alert=Restricted Permissions - {0} actions are not allowed -getPdfInfo.summary.all.permissions.alert=All Permissions Allowed -getPdfInfo.summary.compliance.alert={0} Compliant -getPdfInfo.summary.no.compliance.alert=No Compliance Standards -getPdfInfo.summary.security.section=Security Status -getPdfInfo.section.BasicInfo=Basic Information about the PDF document including file size, page count, and language -getPdfInfo.section.Metadata=Document metadata including title, author, creation date and other document properties -getPdfInfo.section.DocumentInfo=Technical details about the PDF document structure and version -getPdfInfo.section.Compliancy=PDF standards compliance information (PDF/A, PDF/X, etc.) -getPdfInfo.section.Encryption=Security and encryption details of the document -getPdfInfo.section.Permissions=Document permission settings that control what actions can be performed -getPdfInfo.section.Other=Additional document components like bookmarks, layers, and embedded files -getPdfInfo.section.FormFields=Interactive form fields present in the document -getPdfInfo.section.PerPageInfo=Detailed information about each page in the document +getPdfInfo.summary.compliance=Este PDF cumple con el estándar {0} +getPdfInfo.summary.basicInfo=Información básica +getPdfInfo.summary.docInfo=Información del documento +getPdfInfo.summary.encrypted.alert=PDF cifrado - Este documento está protegido con contraseña +getPdfInfo.summary.not.encrypted.alert=PDF no cifrado - Sin protección de contraseña +getPdfInfo.summary.permissions.alert=Permisos restringidos - las acciones {0} no están permitidas +getPdfInfo.summary.all.permissions.alert=Todos los permisos permitidos +getPdfInfo.summary.compliance.alert=Cumple con {0} +getPdfInfo.summary.no.compliance.alert=No cumple con ningún estándar +getPdfInfo.summary.security.section=Estado de seguridad +getPdfInfo.section.BasicInfo=Información básica sobre el documento PDF, incluyendo el tamaño del archivo, número de páginas e idioma +getPdfInfo.section.Metadata=Metadatos del documento, incluyendo el título, autor, fecha de creación y otras propiedades del documento +getPdfInfo.section.DocumentInfo=Detalles técnicos sobre la estructura del documento PDF y su versión +getPdfInfo.section.Compliancy=Información de cumplimiento de estándares de PDF (PDF/A, PDF/X, etc.) +getPdfInfo.section.Encryption=Detalles de seguridad y cifrado del documento +getPdfInfo.section.Permissions=Configuración de los permisos del documento, que controla qué acciones se pueden realizar +getPdfInfo.section.Other=Componentes del documento adicionales, como marcadores, capas y archivos incrustados +getPdfInfo.section.FormFields=Campos de formularios interactivos presentes en el documento +getPdfInfo.section.PerPageInfo=Información detallada sobre cada página del documento #markdown-to-pdf @@ -1081,7 +1105,7 @@ addPageNumbers.selectText.5=Páginas a numerar addPageNumbers.selectText.6=Texto personalizado addPageNumbers.customTextDesc=Texto personalizado addPageNumbers.numberPagesDesc=Qué páginas numerar, por defecto 'todas', también acepta 1-5 o 2,5,9 etc -addPageNumbers.customNumberDesc=Por defecto a {n}, también acepta 'Página {n} de {total}', 'Texto-{n}', '{filename}-{n} +addPageNumbers.customNumberDesc=Por defecto a {n}, también acepta 'Página {n} de {total}', 'Texto-{n}', '{filename}-{n}' addPageNumbers.submit=Añadir Números de Página @@ -1217,6 +1241,7 @@ sign.previous=Página anterior sign.maintainRatio=Activar/desactivar la relación de aspecto sign.undo=Deshacer sign.redo=Rehacer +sign.colour=Signature Colour #repair repair.title=Reparar @@ -1287,7 +1312,7 @@ compress.title=Comprimir compress.header=Comprimir PDF compress.credit=Este servicio utiliza qpdf para compresión/optimización de PDF compress.grayscale.label=Aplicar escala de grises para compresión -compress.selectText.1=Compression Settings +compress.selectText.1=Configuración de la compresión compress.selectText.1.1=1-3 compresión PDF,
4-6 compresión de imagen suave,
7-9 compresión de imágenes intensa reducirá drásticamente la calidad de imagen compress.selectText.2=Nivel de optimización: compress.selectText.4=Modo automático: ajusta automáticamente la calidad para que el PDF tenga el tamaño exacto @@ -1303,11 +1328,11 @@ addImage.upload=Añadir imagen addImage.submit=Enviar imagen #attachments -attachments.title=Add Attachments -attachments.header=Add attachments -attachments.description=Allows you to add attachments to the PDF -attachments.descriptionPlaceholder=Enter a description for the attachments... -attachments.addButton=Add Attachments +attachments.title=Añadir adjuntos +attachments.header=Añadir adjuntos +attachments.description=Le permite añadir adjuntos al PDF +attachments.descriptionPlaceholder=Introduzca una descripción para los adjuntos... +attachments.addButton=Añadir adjuntos #merge merge.title=Unir @@ -1423,6 +1448,34 @@ imageToPDF.selectText.3=Lógica de archivos múltiples (únicamente activado si imageToPDF.selectText.4=Unir en un único archivo PDF imageToPDF.selectText.5=Convertir a PDFs separados +#cbzToPDF +cbzToPDF.title=CBZ to PDF +cbzToPDF.header=CBZ to PDF +cbzToPDF.submit=Convert to PDF +cbzToPDF.selectText=Select CBZ file +cbzToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBZ +pdfToCBZ.title=PDF to CBZ +pdfToCBZ.header=PDF to CBZ +pdfToCBZ.submit=Convert to CBZ +pdfToCBZ.selectText=Select PDF file +pdfToCBZ.dpi=DPI (Dots Per Inch) + +#cbrToPDF +cbrToPDF.title=CBR to PDF +cbrToPDF.header=CBR to PDF +cbrToPDF.submit=Convert to PDF +cbrToPDF.selectText=Select CBR file +cbrToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBR +pdfToCBR.title=PDF to CBR +pdfToCBR.header=PDF to CBR +pdfToCBR.submit=Convert to CBR +pdfToCBR.selectText=Select PDF file +pdfToCBR.dpi=DPI (Dots Per Inch) +pdfToCBR.dpiHelp=Higher DPI results in better quality but larger file size. #pdfToImage pdfToImage.title=PDF a Imagen @@ -1439,12 +1492,13 @@ pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Convertir pdfToImage.info=Python no está instalado. Se requiere para la conversión WebP. pdfToImage.placeholder=(por ejemplo 1,2,8 o 4,7,12-16 o 2n-1) +pdfToImage.includeAnnotations=Include annotations (comments, highlights etc.) #addPassword addPassword.title=Añadir contraseña -addPassword.header=Añadir contraseña (encriptar) -addPassword.selectText.1=Seleccionar PDF para encriptar +addPassword.header=Añadir contraseña (cifrar) +addPassword.selectText.1=Seleccionar PDF para cifrar addPassword.selectText.2=Contraseña addPassword.selectText.3=Longitud de la clave de cifrado addPassword.selectText.4=Valores altos son más fuertes, pero valores bajos tienen mejor compatibilidad @@ -1460,7 +1514,7 @@ addPassword.selectText.13=Impedir imprimir diferentes formatos addPassword.selectText.14=Contraseña addPassword.selectText.15=Restringir qué se puede hacer con el documento una vez abierto (no soportado por todos los lectores) addPassword.selectText.16=Restringir la apertura del propio documento -addPassword.submit=Encriptar +addPassword.submit=Cifrar #watermark @@ -1501,8 +1555,8 @@ permissions.submit=Cambiar #remove password removePassword.title=Eliminar contraseña -removePassword.header=Eliminar contraseña (desencriptar) -removePassword.selectText.1=Seleccionar PDF para desencriptar +removePassword.header=Eliminar contraseña (descifrar) +removePassword.selectText.1=Seleccionar PDF para descifrar removePassword.selectText.2=Contraseña removePassword.submit=Eliminar @@ -1526,14 +1580,14 @@ changeMetadata.selectText.5=Agregar entrada de metadatos personalizados changeMetadata.submit=Cambiar #unlockPDFForms -unlockPDFForms.title=Remove Read-Only from Form Fields -unlockPDFForms.header=Unlock PDF Forms -unlockPDFForms.submit=Remove +unlockPDFForms.title=Eliminar Solo-lectura de los campos de los formularios +unlockPDFForms.header=Desbloquear lso formlarios del PDF +unlockPDFForms.submit=Eliminar #pdfToPDFA pdfToPDFA.title=PDF a PDF/A pdfToPDFA.header=PDF a PDF/A -pdfToPDFA.credit=Este servicio usa libreoffice para la conversión a PDF/A +pdfToPDFA.credit=Este servicio usa LibreOffice para la conversión a PDF/A pdfToPDFA.submit=Convertir pdfToPDFA.tip=Actualmente no funciona para múltiples entrada a la vez pdfToPDFA.outputFormat=Formato de salida @@ -1698,14 +1752,14 @@ fileChooser.dragAndDropPDF=Arrastrar & Soltar archivo PDF fileChooser.dragAndDropImage=Arrastrar & Soltar archivo de Imagen fileChooser.hoveredDragAndDrop=Arrastrar & Soltar archivos(s) aquí fileChooser.extractPDF=Extrayendo... -fileChooser.addAttachments=drag & drop attachments here +fileChooser.addAttachments=arrastrar y soltar adjuntos aquí #release notes releases.footer=Versiones releases.title=Notas de la versión releases.header=Notas de la versión -releases.current.version=versión Actual -releases.note=Las notas de la versión solo están disponibles en Inglés +releases.current.version=versión actual +releases.note=Las notas de la versión solo están disponibles en inglés #Validate Signature validateSignature.title=Validar firmas del PDF @@ -1743,82 +1797,82 @@ validateSignature.cert.selfSigned=Autofirmado validateSignature.cert.bits=bits # Audit Dashboard -audit.dashboard.title=Audit Dashboard -audit.dashboard.systemStatus=Audit System Status -audit.dashboard.status=Status -audit.dashboard.enabled=Enabled -audit.dashboard.disabled=Disabled -audit.dashboard.currentLevel=Current Level -audit.dashboard.retentionPeriod=Retention Period -audit.dashboard.days=days -audit.dashboard.totalEvents=Total Events +audit.dashboard.title=Tablero de auditoria +audit.dashboard.systemStatus=Estado del sistema de auditoria +audit.dashboard.status=Estado +audit.dashboard.enabled=Activado +audit.dashboard.disabled=Desactivado +audit.dashboard.currentLevel=Nivel actual +audit.dashboard.retentionPeriod=Periodo de retention +audit.dashboard.days=días +audit.dashboard.totalEvents=Número total de events # Audit Dashboard Tabs -audit.dashboard.tab.dashboard=Dashboard -audit.dashboard.tab.events=Audit Events -audit.dashboard.tab.export=Export +audit.dashboard.tab.dashboard=Tablero +audit.dashboard.tab.events=Eventos de auditoria +audit.dashboard.tab.export=Exportar # Dashboard Charts -audit.dashboard.eventsByType=Events by Type -audit.dashboard.eventsByUser=Events by User -audit.dashboard.eventsOverTime=Events Over Time -audit.dashboard.period.7days=7 Days -audit.dashboard.period.30days=30 Days -audit.dashboard.period.90days=90 Days +audit.dashboard.eventsByType=Eventos por tipo +audit.dashboard.eventsByUser=Eventos por usuario +audit.dashboard.eventsOverTime=Eventos a lo largo del tiempo +audit.dashboard.period.7days=7 días +audit.dashboard.period.30days=30 días +audit.dashboard.period.90days=90 días # Events Tab -audit.dashboard.auditEvents=Audit Events -audit.dashboard.filter.eventType=Event Type -audit.dashboard.filter.allEventTypes=All event types -audit.dashboard.filter.user=User -audit.dashboard.filter.userPlaceholder=Filter by user -audit.dashboard.filter.startDate=Start Date -audit.dashboard.filter.endDate=End Date -audit.dashboard.filter.apply=Apply Filters -audit.dashboard.filter.reset=Reset Filters +audit.dashboard.auditEvents=Eventos de auditoria +audit.dashboard.filter.eventType=Tipo de evento +audit.dashboard.filter.allEventTypes=Todos los tipos de eventos +audit.dashboard.filter.user=Usuario +audit.dashboard.filter.userPlaceholder=Filtrado por usuario +audit.dashboard.filter.startDate=Fecha inicial +audit.dashboard.filter.endDate=Fecha final +audit.dashboard.filter.apply=Aplicar filtros +audit.dashboard.filter.reset=Reiniciar filtros # Table Headers audit.dashboard.table.id=ID -audit.dashboard.table.time=Time -audit.dashboard.table.user=User -audit.dashboard.table.type=Type -audit.dashboard.table.details=Details -audit.dashboard.table.viewDetails=View Details +audit.dashboard.table.time=Hora +audit.dashboard.table.user=Usuario +audit.dashboard.table.type=Tipo +audit.dashboard.table.details=Detalles +audit.dashboard.table.viewDetails=Ver detalles # Pagination -audit.dashboard.pagination.show=Show -audit.dashboard.pagination.entries=entries -audit.dashboard.pagination.pageInfo1=Page -audit.dashboard.pagination.pageInfo2=of -audit.dashboard.pagination.totalRecords=Total records: +audit.dashboard.pagination.show=Mostrar +audit.dashboard.pagination.entries=entradas +audit.dashboard.pagination.pageInfo1=Página +audit.dashboard.pagination.pageInfo2=de +audit.dashboard.pagination.totalRecords=Registros totales: # Modal -audit.dashboard.modal.eventDetails=Event Details +audit.dashboard.modal.eventDetails=Detalles del evento audit.dashboard.modal.id=ID -audit.dashboard.modal.user=User -audit.dashboard.modal.type=Type -audit.dashboard.modal.time=Time -audit.dashboard.modal.data=Data +audit.dashboard.modal.user=Usuario +audit.dashboard.modal.type=Tipo +audit.dashboard.modal.time=Hora +audit.dashboard.modal.data=Datos # Export Tab -audit.dashboard.export.title=Export Audit Data -audit.dashboard.export.format=Export Format +audit.dashboard.export.title=Exportar datos de auditoria +audit.dashboard.export.format=Formato de exportación audit.dashboard.export.csv=CSV (Comma Separated Values) audit.dashboard.export.json=JSON (JavaScript Object Notation) -audit.dashboard.export.button=Export Data -audit.dashboard.export.infoTitle=Export Information -audit.dashboard.export.infoDesc1=The export will include all audit events matching the selected filters. For large datasets, the export may take a few moments to generate. -audit.dashboard.export.infoDesc2=Exported data will include: -audit.dashboard.export.infoItem1=Event ID -audit.dashboard.export.infoItem2=User -audit.dashboard.export.infoItem3=Event Type -audit.dashboard.export.infoItem4=Timestamp -audit.dashboard.export.infoItem5=Event Data +audit.dashboard.export.button=Exportar datos +audit.dashboard.export.infoTitle=Información de exportación +audit.dashboard.export.infoDesc1=La exportación incluirá todos los eventos de auditoría que cumplan con los filtros seleccionados. Para conjuntos de datos grandes, la generación de la exportación puede llevar un tiempo. +audit.dashboard.export.infoDesc2=Los datos exportados incluirán los siguientes campos: +audit.dashboard.export.infoItem1=ID del evento +audit.dashboard.export.infoItem2=Usuario +audit.dashboard.export.infoItem3=Tipo de evento +audit.dashboard.export.infoItem4=Hora del evento +audit.dashboard.export.infoItem5=Datos del evento # JavaScript i18n keys -audit.dashboard.js.noEventsFound=No audit events found matching the current filters -audit.dashboard.js.errorLoading=Error loading data: -audit.dashboard.js.errorRendering=Error rendering table: -audit.dashboard.js.loadingPage=Loading page +audit.dashboard.js.noEventsFound=No se han encontrado eventos de auditoría que cumplan los filtros actuales +audit.dashboard.js.errorLoading=Error cargando datos: +audit.dashboard.js.errorRendering=Error generando la tabla: +audit.dashboard.js.loadingPage=Cargando la página #################### # Cookie banner # @@ -1839,67 +1893,67 @@ cookieBanner.preferencesModal.subtitle=Uso de cookies cookieBanner.preferencesModal.description.1=Stirling PDF utiliza cookies y tecnologías similares para mejorar su experiencia y entender cómo se usan nuestras herramientas. Esto nos ayuda a mejorar el rendimiento, desarrollar las funciones que le interesan y proporcionar soporte continuo a nuestros usuarios. cookieBanner.preferencesModal.description.2=Stirling PDF no puede—y nunca podrá—rastrear ni acceder al contenido de los documentos que utiliza. cookieBanner.preferencesModal.description.3=Su privacidad y confianza son el núcleo de lo que hacemos. -cookieBanner.preferencesModal.necessary.title.1=Cookies estrictsamente necesarias +cookieBanner.preferencesModal.necessary.title.1=Cookies estrictamente necesarias cookieBanner.preferencesModal.necessary.title.2=Siempre activado cookieBanner.preferencesModal.necessary.description=Estas cookies son esenciales para que el sitio web funcione correctamente. Permiten funciones básicas como configurar sus preferencias de privacidad, iniciar sesión y completar formularios, por lo que no se pueden desactivar. cookieBanner.preferencesModal.analytics.title=Análisis cookieBanner.preferencesModal.analytics.description=Estas cookies nos ayudan a entender cómo se están utilizando nuestras herramientas, para que podamos centrarnos en desarrollar las funciones que nuestra comunidad valora más. Tenga la seguridad de que Stirling PDF no puede y nunca podrá rastrear el contenido de los documentos con los que trabaja. #scannerEffect -scannerEffect.title=Scanner Effect -scannerEffect.header=Scanner Effect -scannerEffect.description=Create a PDF that looks like it was scanned -scannerEffect.selectPDF=Select PDF: -scannerEffect.quality=Scan Quality -scannerEffect.quality.low=Low -scannerEffect.quality.medium=Medium -scannerEffect.quality.high=High -scannerEffect.rotation=Rotation Angle -scannerEffect.rotation.none=None -scannerEffect.rotation.slight=Slight -scannerEffect.rotation.moderate=Moderate -scannerEffect.rotation.severe=Severe -scannerEffect.submit=Create Scanner Effect +scannerEffect.title=Efecto de escáner +scannerEffect.header=Efecto de escáner +scannerEffect.description=Crear un a PDF que parezca haber sido escaneado +scannerEffect.selectPDF=Seleccione el PDF: +scannerEffect.quality=Calidad del escaneo: +scannerEffect.quality.low=Baja +scannerEffect.quality.medium=Media +scannerEffect.quality.high=Alta +scannerEffect.rotation=Ángulo de rotación +scannerEffect.rotation.none=Ninguno +scannerEffect.rotation.slight=Ligero +scannerEffect.rotation.moderate=Moderado +scannerEffect.rotation.severe=Severo +scannerEffect.submit=Crear efecto de escáner #home.scannerEffect -home.scannerEffect.title=Scanner Effect -home.scannerEffect.desc=Create a PDF that looks like it was scanned -scannerEffect.tags=scan,simulate,realistic,convert +home.scannerEffect.title=Efecto de escáner +home.scannerEffect.desc=Crear un a PDF que parezca haber sido escaneado +scannerEffect.tags=escaner,simular,realístico,conversión # ScannerEffect advanced settings (frontend) -scannerEffect.advancedSettings=Enable Advanced Scan Settings -scannerEffect.colorspace=Colorspace -scannerEffect.colorspace.grayscale=Grayscale +scannerEffect.advancedSettings=Activar configuración avanzada de escaneo +scannerEffect.colorspace=Espacio de color +scannerEffect.colorspace.grayscale=Escala de grises scannerEffect.colorspace.color=Color -scannerEffect.border=Border (px) -scannerEffect.rotate=Base Rotation (degrees) -scannerEffect.rotateVariance=Rotation Variance (degrees) -scannerEffect.brightness=Brightness -scannerEffect.contrast=Contrast -scannerEffect.blur=Blur -scannerEffect.noise=Noise -scannerEffect.yellowish=Yellowish (simulate old paper) -scannerEffect.resolution=Resolution (DPI) +scannerEffect.border=Borde (px) +scannerEffect.rotate=Rotación de base (grados) +scannerEffect.rotateVariance=Varianza de rotación (grados) +scannerEffect.brightness=Brillo +scannerEffect.contrast=Contraste +scannerEffect.blur=Difuminado +scannerEffect.noise=Ruido +scannerEffect.yellowish=Amarilleado (simular papel viejo) +scannerEffect.resolution=Resolución (DPI) # Table of Contents Feature -home.editTableOfContents.title=Edit Table of Contents -home.editTableOfContents.desc=Add or edit bookmarks and table of contents in PDF documents +home.editTableOfContents.title=Editar tabla de contenidos +home.editTableOfContents.desc=Añadir o editar marcadores y tabla de contenido en documentos PDF -editTableOfContents.tags=bookmarks,toc,navigation,index,table of contents,chapters,sections,outline -editTableOfContents.title=Edit Table of Contents -editTableOfContents.header=Add or Edit PDF Table of Contents -editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to append to existing) -editTableOfContents.editorTitle=Bookmark Editor -editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks. -editTableOfContents.addBookmark=Add New Bookmark -editTableOfContents.importBookmarksDefault=Import -editTableOfContents.importBookmarksFromJsonFile=Upload JSON file -editTableOfContents.importBookmarksFromClipboard=Paste from clipboard -editTableOfContents.exportBookmarksDefault=Export -editTableOfContents.exportBookmarksAsJson=Download as JSON -editTableOfContents.exportBookmarksAsText=Copy as text -editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document. -editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks. -editTableOfContents.desc.3=Each bookmark requires a title and target page number. -editTableOfContents.submit=Apply Table of Contents +editTableOfContents.tags=marcadores,tdc,navegación,índice,tabla de contenidos,capítulos,secciones,esquema +editTableOfContents.title=Editar tabla de contenidos +editTableOfContents.header=Añadir o editar la tabla de contenidos del PDF +editTableOfContents.replaceExisting=Reemplazar los marcadores existentes (desmarcar para añadir a los existentes) +editTableOfContents.editorTitle=Editor de marcadores +editTableOfContents.editorDesc=Añadir y ordenar los marcadores más abajo. Click + para añadir marcadores hijo. +editTableOfContents.addBookmark=Añadir un nuevo marcador +editTableOfContents.importBookmarksDefault=Importar +editTableOfContents.importBookmarksFromJsonFile=Subir un archivo JSON +editTableOfContents.importBookmarksFromClipboard=Pegar del portapapeles +editTableOfContents.exportBookmarksDefault=Exportar +editTableOfContents.exportBookmarksAsJson=Descargar como JSON +editTableOfContents.exportBookmarksAsText=Copiar como texto +editTableOfContents.desc.1=Esta herramienta le permite añadir o editar la tabla de contenidos (marcadores) de un documento PDF. +editTableOfContents.desc.2=Puede crear una estructura jerárquica añadiendo marcadores hijo a los marcadores padre. +editTableOfContents.desc.3=Cada marcador necesita de un título y un número de pagina destino. +editTableOfContents.submit=Aplicar tabla de contenidos diff --git a/app/core/src/main/resources/messages_eu_ES.properties b/app/core/src/main/resources/messages_eu_ES.properties index 17bd70a93..8367c1b28 100644 --- a/app/core/src/main/resources/messages_eu_ES.properties +++ b/app/core/src/main/resources/messages_eu_ES.properties @@ -137,6 +137,7 @@ lang.yor=Yoruba addPageNumbers.fontSize=Font Size addPageNumbers.fontName=Font Name +addPageNumbers.fontColor=Font Colour pdfPrompt=Hautatu PDFa(k) multiPdfPrompt=Hautatu PDFak (2+) multiPdfDropPrompt=Hautatu (edo arrastatu eta jaregin) nahi dituzun PDFak @@ -193,6 +194,7 @@ error.fileFormatRequired=File must be in {0} format error.invalidFormat=Invalid {0} format: {1} error.endpointDisabled=This endpoint has been disabled by the admin error.urlNotReachable=URL is not reachable, please provide a valid URL +error.invalidUrlFormat=Invalid URL format provided. The provided format is invalid. # DPI and image rendering messages - used by frontend for dynamic translation # Backend sends: [TRANSLATE:messageKey:arg1,arg2] English message @@ -604,6 +606,22 @@ home.imageToPdf.title=Irudia PDF bihurtu home.imageToPdf.desc=Irudi bat(PNG, JPEG, GIF)PDF bihurtu imageToPdf.tags=conversion,img,jpg,picture,photo,psd,photoshop +home.cbzToPdf.title=CBZ to PDF +home.cbzToPdf.desc=Convert CBZ comic book archives to PDF format. +cbzToPdf.tags=conversion,comic,book,archive,cbz,zip + +home.cbrToPdf.title=CBR to PDF +home.cbrToPdf.desc=Convert CBR comic book archives to PDF format. +cbrToPdf.tags=conversion,comic,book,archive,cbr,rar + +home.pdfToCbz.title=PDF to CBZ +home.pdfToCbz.desc=Convert PDF files to CBZ comic book archives. +pdfToCbz.tags=conversion,comic,book,archive,cbz,pdf + +home.pdfToCbr.title=PDF to CBR +home.pdfToCbr.desc=Convert PDF files to CBR comic book archives. +pdfToCbr.tags=conversion,comic,book,archive,cbr,rar + home.pdfToImage.title=PDFa irudi bihurtu home.pdfToImage.desc=PDF bat irudi (PNG, JPEG, GIF) bihurtu pdfToImage.tags=conversion,img,jpg,picture,photo,psd,photoshop @@ -876,6 +894,12 @@ replace-color.selectText.8=Yellow text on black background replace-color.selectText.9=Green text on black background replace-color.selectText.10=Choose text Color replace-color.selectText.11=Choose background Color +replace-color.selectText.12=Colour Space Conversion (CMYK for Printing) +replace-color.selectText.13=CMYK Colour Space Conversion +replace-color.selectText.14=This option converts the PDF from RGB colour space to CMYK colour space, which is optimized for professional printing. This process: +replace-color.selectText.15=Converts colours to CMYK (Cyan, Magenta, Yellow, Black) colour model used by professional printers +replace-color.selectText.16=Optimizes the PDF for print production with prepress settings +replace-color.selectText.17=May result in slight colour changes as CMYK has a smaller colour gamut than RGB replace-color.submit=Replace @@ -1217,6 +1241,7 @@ sign.previous=Previous page sign.maintainRatio=Toggle maintain aspect ratio sign.undo=Undo sign.redo=Redo +sign.colour=Signature Colour #repair repair.title=Konpondu @@ -1423,6 +1448,34 @@ imageToPDF.selectText.3=Fitxategi askoren logika (gaituta bakarrik zenbait irudi imageToPDF.selectText.4=Elkartu PDF bakar batean imageToPDF.selectText.5=Bihurtu eta PDF bereizituak sortu +#cbzToPDF +cbzToPDF.title=CBZ to PDF +cbzToPDF.header=CBZ to PDF +cbzToPDF.submit=Convert to PDF +cbzToPDF.selectText=Select CBZ file +cbzToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBZ +pdfToCBZ.title=PDF to CBZ +pdfToCBZ.header=PDF to CBZ +pdfToCBZ.submit=Convert to CBZ +pdfToCBZ.selectText=Select PDF file +pdfToCBZ.dpi=DPI (Dots Per Inch) + +#cbrToPDF +cbrToPDF.title=CBR to PDF +cbrToPDF.header=CBR to PDF +cbrToPDF.submit=Convert to PDF +cbrToPDF.selectText=Select CBR file +cbrToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBR +pdfToCBR.title=PDF to CBR +pdfToCBR.header=PDF to CBR +pdfToCBR.submit=Convert to CBR +pdfToCBR.selectText=Select PDF file +pdfToCBR.dpi=DPI (Dots Per Inch) +pdfToCBR.dpiHelp=Higher DPI results in better quality but larger file size. #pdfToImage pdfToImage.title=PDFa irudi bihurtu @@ -1439,6 +1492,7 @@ pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Bihurtu pdfToImage.info=Python is not installed. Required for WebP conversion. pdfToImage.placeholder=(e.g. 1,2,8 or 4,7,12-16 or 2n-1) +pdfToImage.includeAnnotations=Include annotations (comments, highlights etc.) #addPassword diff --git a/app/core/src/main/resources/messages_fa_IR.properties b/app/core/src/main/resources/messages_fa_IR.properties index a3b7cfec3..19cd0215f 100644 --- a/app/core/src/main/resources/messages_fa_IR.properties +++ b/app/core/src/main/resources/messages_fa_IR.properties @@ -137,6 +137,7 @@ lang.yor=Yoruba addPageNumbers.fontSize=اندازه فونت addPageNumbers.fontName=نام فونت +addPageNumbers.fontColor=Font Colour pdfPrompt=انتخاب فایل(های) PDF multiPdfPrompt=انتخاب فایل‌های PDF (دو یا بیشتر) multiPdfDropPrompt=انتخاب (یا کشیدن و رها کردن) تمام فایل‌های PDF مورد نیاز @@ -193,6 +194,7 @@ error.fileFormatRequired=File must be in {0} format error.invalidFormat=Invalid {0} format: {1} error.endpointDisabled=This endpoint has been disabled by the admin error.urlNotReachable=URL is not reachable, please provide a valid URL +error.invalidUrlFormat=Invalid URL format provided. The provided format is invalid. # DPI and image rendering messages - used by frontend for dynamic translation # Backend sends: [TRANSLATE:messageKey:arg1,arg2] English message @@ -604,6 +606,22 @@ home.imageToPdf.title=تصویر به PDF home.imageToPdf.desc=تبدیل یک تصویر (PNG، JPEG، GIF) به PDF. imageToPdf.tags=تبدیل،عکس،jpg،تصویر،عکس +home.cbzToPdf.title=CBZ to PDF +home.cbzToPdf.desc=Convert CBZ comic book archives to PDF format. +cbzToPdf.tags=conversion,comic,book,archive,cbz,zip + +home.cbrToPdf.title=CBR to PDF +home.cbrToPdf.desc=Convert CBR comic book archives to PDF format. +cbrToPdf.tags=conversion,comic,book,archive,cbr,rar + +home.pdfToCbz.title=PDF to CBZ +home.pdfToCbz.desc=Convert PDF files to CBZ comic book archives. +pdfToCbz.tags=conversion,comic,book,archive,cbz,pdf + +home.pdfToCbr.title=PDF to CBR +home.pdfToCbr.desc=Convert PDF files to CBR comic book archives. +pdfToCbr.tags=conversion,comic,book,archive,cbr,rar + home.pdfToImage.title=PDF به تصویر home.pdfToImage.desc=تبدیل یک فایل PDF به یک تصویر. (PNG، JPEG، GIF) pdfToImage.tags=تبدیل،عکس،jpg،تصویر،عکس @@ -876,6 +894,12 @@ replace-color.selectText.8=متن زرد روی پس‌زمینه سیاه replace-color.selectText.9=متن سبز روی پس‌زمینه سیاه replace-color.selectText.10=انتخاب رنگ متن replace-color.selectText.11=انتخاب رنگ پس‌زمینه +replace-color.selectText.12=Colour Space Conversion (CMYK for Printing) +replace-color.selectText.13=CMYK Colour Space Conversion +replace-color.selectText.14=This option converts the PDF from RGB colour space to CMYK colour space, which is optimized for professional printing. This process: +replace-color.selectText.15=Converts colours to CMYK (Cyan, Magenta, Yellow, Black) colour model used by professional printers +replace-color.selectText.16=Optimizes the PDF for print production with prepress settings +replace-color.selectText.17=May result in slight colour changes as CMYK has a smaller colour gamut than RGB replace-color.submit=جایگزینی @@ -1217,6 +1241,7 @@ sign.previous=صفحه قبلی sign.maintainRatio=Toggle maintain aspect ratio sign.undo=Undo sign.redo=Redo +sign.colour=Signature Colour #repair repair.title=تعمیر @@ -1423,6 +1448,34 @@ imageToPDF.selectText.3=منطق چند فایل (فقط در صورت کار ب imageToPDF.selectText.4=ادغام در یک PDF واحد imageToPDF.selectText.5=تبدیل به PDF های جداگانه +#cbzToPDF +cbzToPDF.title=CBZ to PDF +cbzToPDF.header=CBZ to PDF +cbzToPDF.submit=Convert to PDF +cbzToPDF.selectText=Select CBZ file +cbzToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBZ +pdfToCBZ.title=PDF to CBZ +pdfToCBZ.header=PDF to CBZ +pdfToCBZ.submit=Convert to CBZ +pdfToCBZ.selectText=Select PDF file +pdfToCBZ.dpi=DPI (Dots Per Inch) + +#cbrToPDF +cbrToPDF.title=CBR to PDF +cbrToPDF.header=CBR to PDF +cbrToPDF.submit=Convert to PDF +cbrToPDF.selectText=Select CBR file +cbrToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBR +pdfToCBR.title=PDF to CBR +pdfToCBR.header=PDF to CBR +pdfToCBR.submit=Convert to CBR +pdfToCBR.selectText=Select PDF file +pdfToCBR.dpi=DPI (Dots Per Inch) +pdfToCBR.dpiHelp=Higher DPI results in better quality but larger file size. #pdfToImage pdfToImage.title=PDF به تصویر @@ -1439,6 +1492,7 @@ pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=تبدیل pdfToImage.info=پایتون نصب نشده است. برای تبدیل WebP لازم است. pdfToImage.placeholder=(مثال: 1,2,8 یا 4,7,12-16 یا 2n-1) +pdfToImage.includeAnnotations=Include annotations (comments, highlights etc.) #addPassword diff --git a/app/core/src/main/resources/messages_fr_FR.properties b/app/core/src/main/resources/messages_fr_FR.properties index b9db7ff5c..98e8c1356 100644 --- a/app/core/src/main/resources/messages_fr_FR.properties +++ b/app/core/src/main/resources/messages_fr_FR.properties @@ -137,6 +137,7 @@ lang.yor=Yoruba addPageNumbers.fontSize=Taille de Police addPageNumbers.fontName=Nom de la Police +addPageNumbers.fontColor=Font Colour pdfPrompt=Sélectionnez le(s) PDF multiPdfPrompt=Sélectionnez les PDF multiPdfDropPrompt=Sélectionnez (ou glissez-déposez) tous les PDF dont vous avez besoin @@ -193,6 +194,7 @@ error.fileFormatRequired=Le fichier doit être au format {0} error.invalidFormat=Format {0} invalide : {1} error.endpointDisabled=Ce point de terminaison a été désactivé par l'administrateur error.urlNotReachable=L'URL est inaccessible, veuillez fournir une URL valide +error.invalidUrlFormat=Invalid URL format provided. The provided format is invalid. # DPI and image rendering messages - used by frontend for dynamic translation # Backend sends: [TRANSLATE:messageKey:arg1,arg2] English message @@ -368,36 +370,36 @@ settings.update=Mise à jour disponible settings.updateAvailable={0} est la version actuellement installée. Une nouvelle version ({1}) est disponible. # Update modal and notification strings -update.urgentUpdateAvailable=🚨 Update Available -update.updateAvailable=Update Available -update.modalTitle=Update Available -update.current=Current -update.latest=Latest -update.latestStable=Latest Stable -update.priority=Priority -update.recommendedAction=Recommended Action -update.breakingChangesDetected=⚠️ Breaking Changes Detected -update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. -update.migrationGuides=Migration Guides: -update.viewGuide=View Guide -update.loadingDetailedInfo=Loading detailed version information... -update.close=Close -update.viewAllReleases=View All Releases -update.downloadLatest=Download Latest -update.availableUpdates=Available Updates: -update.unableToLoadDetails=Unable to load detailed version information. +update.urgentUpdateAvailable=🚨 Mise à jour disponible +update.updateAvailable=Mise à jour disponible +update.modalTitle=Mise à jour disponible +update.current=Courante +update.latest=Dernière +update.latestStable=Dernière version stable +update.priority=Priorité +update.recommendedAction=Action recommandée +update.breakingChangesDetected=⚠️ Changement critique +update.breakingChangesMessage=Cette mise à jour contient des changements critiques. Veuillez suivre les indications des guides de migration ci-dessous. +update.migrationGuides=Guides de Migration : +update.viewGuide=Lire le Guide +update.loadingDetailedInfo=Chargement des informations de version détaillées... +update.close=Fermer +update.viewAllReleases=Voir toutes les versions +update.downloadLatest=Télécharger la plus récente +update.availableUpdates=Mises à jour disponibles: +update.unableToLoadDetails=Impossible de charger les informations de version détaillées. update.version=Version # Update priority levels -update.priority.urgent=URGENT -update.priority.normal=NORMAL -update.priority.minor=MINOR -update.priority.low=LOW +update.priority.urgent=URGENTE +update.priority.normal=NORMALE +update.priority.minor=MINEURE +update.priority.low=BASSE # Breaking changes text -update.breakingChanges=Breaking Changes: -update.breakingChangesDefault=This version contains breaking changes -update.migrationGuide=Migration Guide +update.breakingChanges=Changements critiques: +update.breakingChangesDefault=Cette version contient des changements critiques +update.migrationGuide=Guide de migration settings.appVersion=Version de l'application : settings.downloadOption.title=Choisissez l'option de téléchargement (pour les téléchargements à fichier unique non ZIP) : settings.downloadOption.1=Ouvrir dans la même fenêtre @@ -604,6 +606,22 @@ home.imageToPdf.title=Image en PDF home.imageToPdf.desc=Convertissez une image (PNG, JPEG, GIF, PSD) en PDF. imageToPdf.tags=pdf,conversion,img,jpg,image,photo +home.cbzToPdf.title=CBZ to PDF +home.cbzToPdf.desc=Convert CBZ comic book archives to PDF format. +cbzToPdf.tags=conversion,comic,book,archive,cbz,zip + +home.cbrToPdf.title=CBR to PDF +home.cbrToPdf.desc=Convert CBR comic book archives to PDF format. +cbrToPdf.tags=conversion,comic,book,archive,cbr,rar + +home.pdfToCbz.title=PDF to CBZ +home.pdfToCbz.desc=Convert PDF files to CBZ comic book archives. +pdfToCbz.tags=conversion,comic,book,archive,cbz,pdf + +home.pdfToCbr.title=PDF to CBR +home.pdfToCbr.desc=Convert PDF files to CBR comic book archives. +pdfToCbr.tags=conversion,comic,book,archive,cbr,rar + home.pdfToImage.title=PDF en image home.pdfToImage.desc=Convertissez un PDF en image (PNG, JPEG, GIF). pdfToImage.tags=conversion,img,jpg,image,photo @@ -876,6 +894,12 @@ replace-color.selectText.8=Texte jaune sur fond noir replace-color.selectText.9=Texte vert sur fond noir replace-color.selectText.10=Choisir la couleur du texte replace-color.selectText.11=Choisir la couleur de l'arrière-plan +replace-color.selectText.12=Colour Space Conversion (CMYK for Printing) +replace-color.selectText.13=CMYK Colour Space Conversion +replace-color.selectText.14=This option converts the PDF from RGB colour space to CMYK colour space, which is optimized for professional printing. This process: +replace-color.selectText.15=Converts colours to CMYK (Cyan, Magenta, Yellow, Black) colour model used by professional printers +replace-color.selectText.16=Optimizes the PDF for print production with prepress settings +replace-color.selectText.17=May result in slight colour changes as CMYK has a smaller colour gamut than RGB replace-color.submit=Remplacer @@ -1217,6 +1241,7 @@ sign.previous=Page précédente sign.maintainRatio=Conserver les proportions sign.undo=Défaire sign.redo=Refaire +sign.colour=Signature Colour #repair repair.title=Réparer @@ -1423,6 +1448,34 @@ imageToPDF.selectText.3=Logique multi-fichiers (uniquement activée si vous trav imageToPDF.selectText.4=Fusionner en un seul PDF imageToPDF.selectText.5=Convertir en PDF séparés +#cbzToPDF +cbzToPDF.title=CBZ to PDF +cbzToPDF.header=CBZ to PDF +cbzToPDF.submit=Convert to PDF +cbzToPDF.selectText=Select CBZ file +cbzToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBZ +pdfToCBZ.title=PDF to CBZ +pdfToCBZ.header=PDF to CBZ +pdfToCBZ.submit=Convert to CBZ +pdfToCBZ.selectText=Select PDF file +pdfToCBZ.dpi=DPI (Dots Per Inch) + +#cbrToPDF +cbrToPDF.title=CBR to PDF +cbrToPDF.header=CBR to PDF +cbrToPDF.submit=Convert to PDF +cbrToPDF.selectText=Select CBR file +cbrToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBR +pdfToCBR.title=PDF to CBR +pdfToCBR.header=PDF to CBR +pdfToCBR.submit=Convert to CBR +pdfToCBR.selectText=Select PDF file +pdfToCBR.dpi=DPI (Dots Per Inch) +pdfToCBR.dpiHelp=Higher DPI results in better quality but larger file size. #pdfToImage pdfToImage.title=PDF en Image @@ -1435,10 +1488,11 @@ pdfToImage.colorType=Type d'impression pdfToImage.color=Couleur pdfToImage.grey=Niveaux de gris pdfToImage.blackwhite=Noir et blanc (peut engendrer une perte de données !) -pdfToImage.dpi=DPI (The server limit is {0} dpi) +pdfToImage.dpi=DPI (La limite du serveur est {0} DPI) pdfToImage.submit=Convertir pdfToImage.info=Python n'est pas installé. Nécessaire pour la conversion WebP. pdfToImage.placeholder=(par exemple : 1,2,8 ou 4,7,12-16 ou 2n-1) +pdfToImage.includeAnnotations=Include annotations (comments, highlights etc.) #addPassword @@ -1893,12 +1947,12 @@ editTableOfContents.replaceExisting=Remplacer les signets existants (décocher p editTableOfContents.editorTitle=Éditeur de signets editTableOfContents.editorDesc=Ajoutez et organisez les signets ci-dessous. Cliquez sur + pour ajouter des signets enfants. editTableOfContents.addBookmark=Ajouter un nouveau signet -editTableOfContents.importBookmarksDefault=Import -editTableOfContents.importBookmarksFromJsonFile=Upload JSON file -editTableOfContents.importBookmarksFromClipboard=Paste from clipboard -editTableOfContents.exportBookmarksDefault=Export -editTableOfContents.exportBookmarksAsJson=Download as JSON -editTableOfContents.exportBookmarksAsText=Copy as text +editTableOfContents.importBookmarksDefault=Importer +editTableOfContents.importBookmarksFromJsonFile=Charger un fichier JSON +editTableOfContents.importBookmarksFromClipboard=Coller depuis le presse-papiers +editTableOfContents.exportBookmarksDefault=Exporter +editTableOfContents.exportBookmarksAsJson=Télécharger sous forme de JSON +editTableOfContents.exportBookmarksAsText=Copier comme texte editTableOfContents.desc.1=Cet outil vous permet d'ajouter ou de modifier la table des matières (signets) dans un document PDF. editTableOfContents.desc.2=Vous pouvez créer une structure hiérarchique en ajoutant des signets enfants à des signets parents. editTableOfContents.desc.3=Chaque signet nécessite un titre et un numéro de page cible. diff --git a/app/core/src/main/resources/messages_ga_IE.properties b/app/core/src/main/resources/messages_ga_IE.properties index b0363acb4..3884e031c 100644 --- a/app/core/src/main/resources/messages_ga_IE.properties +++ b/app/core/src/main/resources/messages_ga_IE.properties @@ -137,6 +137,7 @@ lang.yor=Yoruba addPageNumbers.fontSize=Méid an Chló addPageNumbers.fontName=Ainm Cló +addPageNumbers.fontColor=Font Colour pdfPrompt=Roghnaigh PDF(anna) multiPdfPrompt=Roghnaigh PDFs (2+) multiPdfDropPrompt=Roghnaigh (nó tarraing & scaoil) gach PDF atá uait @@ -193,6 +194,7 @@ error.fileFormatRequired=File must be in {0} format error.invalidFormat=Invalid {0} format: {1} error.endpointDisabled=This endpoint has been disabled by the admin error.urlNotReachable=URL is not reachable, please provide a valid URL +error.invalidUrlFormat=Invalid URL format provided. The provided format is invalid. # DPI and image rendering messages - used by frontend for dynamic translation # Backend sends: [TRANSLATE:messageKey:arg1,arg2] English message @@ -604,6 +606,22 @@ home.imageToPdf.title=Íomhá go PDF home.imageToPdf.desc=Tiontaigh íomhá (PNG, JPEG, GIF) go PDF. imageToPdf.tags=comhshó, img, jpg, pictiúr, grianghraf +home.cbzToPdf.title=CBZ to PDF +home.cbzToPdf.desc=Convert CBZ comic book archives to PDF format. +cbzToPdf.tags=conversion,comic,book,archive,cbz,zip + +home.cbrToPdf.title=CBR to PDF +home.cbrToPdf.desc=Convert CBR comic book archives to PDF format. +cbrToPdf.tags=conversion,comic,book,archive,cbr,rar + +home.pdfToCbz.title=PDF to CBZ +home.pdfToCbz.desc=Convert PDF files to CBZ comic book archives. +pdfToCbz.tags=conversion,comic,book,archive,cbz,pdf + +home.pdfToCbr.title=PDF to CBR +home.pdfToCbr.desc=Convert PDF files to CBR comic book archives. +pdfToCbr.tags=conversion,comic,book,archive,cbr,rar + home.pdfToImage.title=PDF go íomhá home.pdfToImage.desc=Tiontaigh PDF a íomhá. (PNG, JPEG, GIF) pdfToImage.tags=comhshó, img, jpg, pictiúr, grianghraf @@ -876,6 +894,12 @@ replace-color.selectText.8=Téacs buí ar chúlra dubh replace-color.selectText.9=Téacs glas ar chúlra dubh replace-color.selectText.10=Roghnaigh Dath an téacs replace-color.selectText.11=Roghnaigh Dath an Chúlra +replace-color.selectText.12=Colour Space Conversion (CMYK for Printing) +replace-color.selectText.13=CMYK Colour Space Conversion +replace-color.selectText.14=This option converts the PDF from RGB colour space to CMYK colour space, which is optimized for professional printing. This process: +replace-color.selectText.15=Converts colours to CMYK (Cyan, Magenta, Yellow, Black) colour model used by professional printers +replace-color.selectText.16=Optimizes the PDF for print production with prepress settings +replace-color.selectText.17=May result in slight colour changes as CMYK has a smaller colour gamut than RGB replace-color.submit=Ionadaigh @@ -1081,7 +1105,7 @@ addPageNumbers.selectText.5=Leathanaigh go hUimhir addPageNumbers.selectText.6=Téacs Saincheaptha addPageNumbers.customTextDesc=Téacs Saincheaptha addPageNumbers.numberPagesDesc=Cé na leathanaigh le huimhriú, réamhshocraithe 'gach duine', a ghlacann freisin 1-5 nó 2,5,9 etc -addPageNumbers.customNumberDesc=Réamhshocrú go {n}, glacann sé freisin le 'Leathanach {n} de {total}', 'Text-{n}', '{filename}-{n} +addPageNumbers.customNumberDesc=Réamhshocrú go {n}, glacann sé freisin le 'Leathanach {n} de {total}', 'Text-{n}', '{filename}-{n}' addPageNumbers.submit=Cuir Uimhreacha Leathanaigh leis @@ -1217,6 +1241,7 @@ sign.previous=Leathanach roimhe seo sign.maintainRatio=Scoránaigh, coinnigh an cóimheas gné sign.undo=Undo sign.redo=Redo +sign.colour=Signature Colour #repair repair.title=Deisiúchán @@ -1423,6 +1448,34 @@ imageToPDF.selectText.3=Loighic ilchomhad (cumasaithe ach amháin má oibríonn imageToPDF.selectText.4=Chumasadh go PDF amháin imageToPDF.selectText.5=Tiontaigh go PDF ar leith +#cbzToPDF +cbzToPDF.title=CBZ to PDF +cbzToPDF.header=CBZ to PDF +cbzToPDF.submit=Convert to PDF +cbzToPDF.selectText=Select CBZ file +cbzToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBZ +pdfToCBZ.title=PDF to CBZ +pdfToCBZ.header=PDF to CBZ +pdfToCBZ.submit=Convert to CBZ +pdfToCBZ.selectText=Select PDF file +pdfToCBZ.dpi=DPI (Dots Per Inch) + +#cbrToPDF +cbrToPDF.title=CBR to PDF +cbrToPDF.header=CBR to PDF +cbrToPDF.submit=Convert to PDF +cbrToPDF.selectText=Select CBR file +cbrToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBR +pdfToCBR.title=PDF to CBR +pdfToCBR.header=PDF to CBR +pdfToCBR.submit=Convert to CBR +pdfToCBR.selectText=Select PDF file +pdfToCBR.dpi=DPI (Dots Per Inch) +pdfToCBR.dpiHelp=Higher DPI results in better quality but larger file size. #pdfToImage pdfToImage.title=PDF go íomhá @@ -1439,6 +1492,7 @@ pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Tiontaigh pdfToImage.info=Níl Python suiteáilte. Ag teastáil le haghaidh comhshó WebP. pdfToImage.placeholder=(m.sh. 1,2,8 nó 4,7,12-16 nó 2n-1) +pdfToImage.includeAnnotations=Include annotations (comments, highlights etc.) #addPassword diff --git a/app/core/src/main/resources/messages_hi_IN.properties b/app/core/src/main/resources/messages_hi_IN.properties index 32885740c..6e23247d4 100644 --- a/app/core/src/main/resources/messages_hi_IN.properties +++ b/app/core/src/main/resources/messages_hi_IN.properties @@ -137,6 +137,7 @@ lang.yor=Yoruba addPageNumbers.fontSize=फ़ॉन्ट आकार addPageNumbers.fontName=फ़ॉन्ट नाम +addPageNumbers.fontColor=Font Colour pdfPrompt=पीडीएफ फ़ाइल(ें) चुनें multiPdfPrompt=पीडीएफ फ़ाइलें चुनें (2+) multiPdfDropPrompt=आवश्यक सभी पीडीएफ फ़ाइलों को चुनें (या खींच कर छोड़ें) @@ -193,6 +194,7 @@ error.fileFormatRequired=File must be in {0} format error.invalidFormat=Invalid {0} format: {1} error.endpointDisabled=This endpoint has been disabled by the admin error.urlNotReachable=URL is not reachable, please provide a valid URL +error.invalidUrlFormat=Invalid URL format provided. The provided format is invalid. # DPI and image rendering messages - used by frontend for dynamic translation # Backend sends: [TRANSLATE:messageKey:arg1,arg2] English message @@ -604,6 +606,22 @@ home.imageToPdf.title=छवि से PDF home.imageToPdf.desc=छवि (PNG, JPEG, GIF) को PDF में बदलें। imageToPdf.tags=रूपांतरण,img,jpg,चित्र,फोटो +home.cbzToPdf.title=CBZ to PDF +home.cbzToPdf.desc=Convert CBZ comic book archives to PDF format. +cbzToPdf.tags=conversion,comic,book,archive,cbz,zip + +home.cbrToPdf.title=CBR to PDF +home.cbrToPdf.desc=Convert CBR comic book archives to PDF format. +cbrToPdf.tags=conversion,comic,book,archive,cbr,rar + +home.pdfToCbz.title=PDF to CBZ +home.pdfToCbz.desc=Convert PDF files to CBZ comic book archives. +pdfToCbz.tags=conversion,comic,book,archive,cbz,pdf + +home.pdfToCbr.title=PDF to CBR +home.pdfToCbr.desc=Convert PDF files to CBR comic book archives. +pdfToCbr.tags=conversion,comic,book,archive,cbr,rar + home.pdfToImage.title=PDF से छवि home.pdfToImage.desc=PDF को छवि में बदलें। (PNG, JPEG, GIF) pdfToImage.tags=रूपांतरण,img,jpg,चित्र,फोटो @@ -876,6 +894,12 @@ replace-color.selectText.8=काली पृष्ठभूमि पर प replace-color.selectText.9=काली पृष्ठभूमि पर हरा टेक्स्ट replace-color.selectText.10=टेक्स्ट रंग चुनें replace-color.selectText.11=पृष्ठभूमि रंग चुनें +replace-color.selectText.12=Colour Space Conversion (CMYK for Printing) +replace-color.selectText.13=CMYK Colour Space Conversion +replace-color.selectText.14=This option converts the PDF from RGB colour space to CMYK colour space, which is optimized for professional printing. This process: +replace-color.selectText.15=Converts colours to CMYK (Cyan, Magenta, Yellow, Black) colour model used by professional printers +replace-color.selectText.16=Optimizes the PDF for print production with prepress settings +replace-color.selectText.17=May result in slight colour changes as CMYK has a smaller colour gamut than RGB replace-color.submit=बदलें @@ -1217,6 +1241,7 @@ sign.previous=पिछला पृष्ठ sign.maintainRatio=आनुपातिक अनुपात बनाए रखें टॉगल करें sign.undo=Undo sign.redo=Redo +sign.colour=Signature Colour #repair repair.title=मरम्मत @@ -1423,6 +1448,34 @@ imageToPDF.selectText.3=बहु फ़ाइल तर्क (केवल क imageToPDF.selectText.4=एकल PDF में मर्ज करें imageToPDF.selectText.5=अलग-अलग PDF में बदलें +#cbzToPDF +cbzToPDF.title=CBZ to PDF +cbzToPDF.header=CBZ to PDF +cbzToPDF.submit=Convert to PDF +cbzToPDF.selectText=Select CBZ file +cbzToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBZ +pdfToCBZ.title=PDF to CBZ +pdfToCBZ.header=PDF to CBZ +pdfToCBZ.submit=Convert to CBZ +pdfToCBZ.selectText=Select PDF file +pdfToCBZ.dpi=DPI (Dots Per Inch) + +#cbrToPDF +cbrToPDF.title=CBR to PDF +cbrToPDF.header=CBR to PDF +cbrToPDF.submit=Convert to PDF +cbrToPDF.selectText=Select CBR file +cbrToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBR +pdfToCBR.title=PDF to CBR +pdfToCBR.header=PDF to CBR +pdfToCBR.submit=Convert to CBR +pdfToCBR.selectText=Select PDF file +pdfToCBR.dpi=DPI (Dots Per Inch) +pdfToCBR.dpiHelp=Higher DPI results in better quality but larger file size. #pdfToImage pdfToImage.title=PDF से छवि @@ -1439,6 +1492,7 @@ pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=बदलें pdfToImage.info=Python स्थापित नहीं है। WebP रूपांतरण के लिए आवश्यक है। pdfToImage.placeholder=(जैसे 1,2,8 या 4,7,12-16 या 2n-1) +pdfToImage.includeAnnotations=Include annotations (comments, highlights etc.) #addPassword diff --git a/app/core/src/main/resources/messages_hr_HR.properties b/app/core/src/main/resources/messages_hr_HR.properties index cb06aba43..7beb74a80 100644 --- a/app/core/src/main/resources/messages_hr_HR.properties +++ b/app/core/src/main/resources/messages_hr_HR.properties @@ -6,358 +6,360 @@ language.direction=ltr # Language names for reuse throughout the application lang.afr=Afrikaans -lang.amh=Amharic -lang.ara=Arabic -lang.asm=Assamese -lang.aze=Azerbaijani -lang.aze_cyrl=Azerbaijani (Cyrillic) -lang.bel=Belarusian -lang.ben=Bengali -lang.bod=Tibetan -lang.bos=Bosnian -lang.bre=Breton -lang.bul=Bulgarian -lang.cat=Catalan +lang.amh=Amharski +lang.ara=Arapski +lang.asm=Asamski +lang.aze=Azerbajdžanski +lang.aze_cyrl=Azerbajdžanski (ćirilica) +lang.bel=Bjeloruski +lang.ben=Bengalski +lang.bod=Tibetanski +lang.bos=Bosanski +lang.bre=Bretonski +lang.bul=Bugarski +lang.cat=Katalonski lang.ceb=Cebuano -lang.ces=Czech -lang.chi_sim=Chinese (Simplified) -lang.chi_sim_vert=Chinese (Simplified, Vertical) -lang.chi_tra=Chinese (Traditional) -lang.chi_tra_vert=Chinese (Traditional, Vertical) +lang.ces=Češki +lang.chi_sim=Kineski (pojednostavljeni) +lang.chi_sim_vert=Kineski (pojednostavljeni, okomito) +lang.chi_tra=Kineski (tradicionalni) +lang.chi_tra_vert=Kineski (tradicionalni, okomito) lang.chr=Cherokee -lang.cos=Corsican -lang.cym=Welsh -lang.dan=Danish -lang.dan_frak=Danish (Fraktur) -lang.deu=German -lang.deu_frak=German (Fraktur) +lang.cos=Korzikanski +lang.cym=Velški +lang.dan=Danski +lang.dan_frak=Danski (fraktura) +lang.deu=Njemački +lang.deu_frak=Njemački (fraktura) lang.div=Divehi lang.dzo=Dzongkha -lang.ell=Greek -lang.eng=English -lang.enm=English, Middle (1100-1500) +lang.ell=Grčki +lang.eng=Engleski +lang.enm=Srednjoengleski (1100-1500) lang.epo=Esperanto -lang.equ=Math / equation detection module -lang.est=Estonian -lang.eus=Basque -lang.fao=Faroese -lang.fas=Persian -lang.fil=Filipino -lang.fin=Finnish -lang.fra=French -lang.frk=Frankish -lang.frm=French, Middle (ca.1400-1600) -lang.fry=Western Frisian -lang.gla=Scottish Gaelic -lang.gle=Irish -lang.glg=Galician -lang.grc=Ancient Greek +lang.equ=Matematika / modul za prepoznavanje jednadžbi +lang.est=Estonski +lang.eus=Baskijski +lang.fao=Farski +lang.fas=Perzijski +lang.fil=Filipinski +lang.fin=Finski +lang.fra=Francuski +lang.frk=Franački +lang.frm=Srednjofrancuski (oko 1400-1600) +lang.fry=Zapadnofrizijski +lang.gla=Škotski galski +lang.gle=Irski +lang.glg=Galicijski +lang.grc=Starogrčki lang.guj=Gujarati -lang.hat=Haitian, Haitian Creole -lang.heb=Hebrew -lang.hin=Hindi -lang.hrv=Croatian -lang.hun=Hungarian -lang.hye=Armenian +lang.hat=Haićanski, haićanski kreolski +lang.heb=Hebrejski +lang.hin=Hindski +lang.hrv=Hrvatski +lang.hun=Mađarski +lang.hye=Armenski lang.iku=Inuktitut -lang.ind=Indonesian -lang.isl=Icelandic -lang.ita=Italian -lang.ita_old=Italian (Old) -lang.jav=Javanese -lang.jpn=Japanese -lang.jpn_vert=Japanese (Vertical) +lang.ind=Indonezijski +lang.isl=Islandski +lang.ita=Talijanski +lang.ita_old=Talijanski (stari) +lang.jav=Javanski +lang.jpn=Japanski +lang.jpn_vert=Japanski (okomito) lang.kan=Kannada -lang.kat=Georgian -lang.kat_old=Georgian (Old) -lang.kaz=Kazakh -lang.khm=Central Khmer -lang.kir=Kirghiz, Kyrgyz -lang.kmr=Northern Kurdish -lang.kor=Korean -lang.kor_vert=Korean (Vertical) -lang.lao=Lao -lang.lat=Latin -lang.lav=Latvian -lang.lit=Lithuanian -lang.ltz=Luxembourgish -lang.mal=Malayalam +lang.kat=Gruzijski +lang.kat_old=Gruzijski (stari) +lang.kaz=Kazaški +lang.khm=Središnji kmerski +lang.kir=Kirgiški +lang.kmr=Sjevernokurdski +lang.kor=Korejski +lang.kor_vert=Korejski (okomito) +lang.lao=Laoski +lang.lat=Latinski +lang.lav=Latvijski +lang.lit=Litavski +lang.ltz=Luksemburški +lang.mal=Malajalamski lang.mar=Marathi -lang.mkd=Macedonian -lang.mlt=Maltese -lang.mon=Mongolian -lang.mri=Maori -lang.msa=Malay -lang.mya=Burmese -lang.nep=Nepali -lang.nld=Dutch; Flemish -lang.nor=Norwegian -lang.oci=Occitan (post 1500) -lang.ori=Oriya -lang.osd=Orientation and script detection module -lang.pan=Panjabi, Punjabi -lang.pol=Polish -lang.por=Portuguese -lang.pus=Pushto, Pashto -lang.que=Quechua -lang.ron=Romanian, Moldavian, Moldovan -lang.rus=Russian -lang.san=Sanskrit -lang.sin=Sinhala, Sinhalese -lang.slk=Slovak -lang.slk_frak=Slovak (Fraktur) -lang.slv=Slovenian +lang.mkd=Makedonski +lang.mlt=Malteški +lang.mon=Mongolski +lang.mri=Maorski +lang.msa=Malajski +lang.mya=Burmanski +lang.nep=Nepalski +lang.nld=Nizozemski; flamanski +lang.nor=Norveški +lang.oci=Okcitanski (nakon 1500.) +lang.ori=Orija +lang.osd=Modul za prepoznavanje orijentacije i pisma +lang.pan=Pandžapski +lang.pol=Poljski +lang.por=Portugalski +lang.pus=Paštunski +lang.que=Kečua +lang.ron=Rumunjski, moldavski +lang.rus=Ruski +lang.san=Sanskrt +lang.sin=Singalski +lang.slk=Slovački +lang.slk_frak=Slovački (fraktura) +lang.slv=Slovenski lang.snd=Sindhi -lang.spa=Spanish -lang.spa_old=Spanish (Old) -lang.sqi=Albanian -lang.srp=Serbian -lang.srp_latn=Serbian (Latin) -lang.sun=Sundanese -lang.swa=Swahili -lang.swe=Swedish -lang.syr=Syriac -lang.tam=Tamil -lang.tat=Tatar +lang.spa=Španjolski +lang.spa_old=Španjolski (stari) +lang.sqi=Albanski +lang.srp=Srpski +lang.srp_latn=Srpski (latinica) +lang.sun=Sundanski +lang.swa=Svahili +lang.swe=Švedski +lang.syr=Sirijski +lang.tam=Tamilski +lang.tat=Tatarski lang.tel=Telugu -lang.tgk=Tajik +lang.tgk=Tadžički lang.tgl=Tagalog -lang.tha=Thai -lang.tir=Tigrinya -lang.ton=Tonga (Tonga Islands) -lang.tur=Turkish -lang.uig=Uighur, Uyghur -lang.ukr=Ukrainian +lang.tha=Tajlandski +lang.tir=Tigrinja +lang.ton=Tonga (otoci Tonga) +lang.tur=Turski +lang.uig=Ujgurski +lang.ukr=Ukrajinski lang.urd=Urdu -lang.uzb=Uzbek -lang.uzb_cyrl=Uzbek (Cyrillic) -lang.vie=Vietnamese -lang.yid=Yiddish -lang.yor=Yoruba +lang.uzb=Uzbečki +lang.uzb_cyrl=Uzbečki (ćirilica) +lang.vie=Vijetnamski +lang.yid=Jidiš +lang.yor=Joruba -addPageNumbers.fontSize=Veličina pisma -addPageNumbers.fontName=Ime pisma -pdfPrompt=Odaberi PDF(ove) -multiPdfPrompt=Odaberi PDF-ove (2+) -multiPdfDropPrompt=Odaberi (ili povuci i ispusti) sve potrebne PDF-ove -imgPrompt=Odaberi sliku (slike) +addPageNumbers.fontSize=Veličina fonta +addPageNumbers.fontName=Naziv fonta +addPageNumbers.fontColor=Font Colour +pdfPrompt=Odaberite PDF datoteku(e) +multiPdfPrompt=Odaberite PDF datoteke (2+) +multiPdfDropPrompt=Odaberite (ili povucite i ispustite) sve potrebne PDF datoteke +imgPrompt=Odaberite sliku(e) genericSubmit=Pošalji -uploadLimit=Maximum file size: -uploadLimitExceededSingular=is too large. Maximum allowed size is -uploadLimitExceededPlural=are too large. Maximum allowed size is -processTimeWarning=Upozorenje: Ovaj proces može trajati i do minutu, u zavisnosti od veličine dokumenta -pageOrderPrompt=Prilagođeni redoslijed stranica (unesi listu brojeva stranica ili funkcija, kao što su 2n+1, razdvojene zarezima) : -pageSelectionPrompt=Prilagođeni odabir stranica (unesi listu brojeva stranica ili funkcija, kao što su 2n+1, razdvojene zarezima) : -goToPage=Idi na stranicu +uploadLimit=Maksimalna veličina datoteke: +uploadLimitExceededSingular=je prevelika. Maksimalna dopuštena veličina je +uploadLimitExceededPlural=su prevelike. Maksimalna dopuštena veličina je +processTimeWarning=Upozorenje: Ovaj proces može potrajati do jedne minute, ovisno o veličini datoteke +pageOrderPrompt=Prilagođeni redoslijed stranica (unesite popis brojeva stranica ili funkcija poput 2n+1, odvojenih zarezom): +pageSelectionPrompt=Prilagođeni odabir stranica (unesite popis brojeva stranica 1,5,6 ili funkcija poput 2n+1, odvojenih zarezom): +goToPage=Idi true=Točno false=Netočno unknown=Nepoznato save=Spremi -saveToBrowser=spremi u Preglednik +saveToBrowser=Spremi u preglednik close=Zatvori -filesSelected=odabrane datoteke +filesSelected=odabranih datoteka noFavourites=Nema dodanih favorita -downloadComplete=Preuzimanje završeno -bored=Dosađujete se čekajući? +downloadComplete=Preuzimanje dovršeno +bored=Dosadno vam je dok čekate? alphabet=Abeceda downloadPdf=Preuzmi PDF text=Tekst -font=Pismo -selectFilter=-- Odaberi -- +font=Font +selectFilter=-- Odaberite -- pageNum=Broj stranice sizes.small=Malo sizes.medium=Srednje sizes.large=Veliko sizes.x-large=Jako veliko -error.pdfPassword=PDF dokument je šifriran i zaporka nije dana ili je netočna -error.pdfCorrupted=PDF file appears to be corrupted or damaged. Please try using the 'Repair PDF' feature first to fix the file before proceeding with this operation. -error.pdfCorruptedMultiple=One or more PDF files appear to be corrupted or damaged. Please try using the 'Repair PDF' feature on each file first before attempting to merge them. -error.pdfCorruptedDuring=Error {0}: PDF file appears to be corrupted or damaged. Please try using the 'Repair PDF' feature first to fix the file before proceeding with this operation. +error.pdfPassword=PDF dokument je zaštićen lozinkom, a lozinka nije unesena ili je netočna +error.pdfCorrupted=Čini se da je PDF datoteka oštećena. Pokušajte prvo upotrijebiti značajku 'Popravi PDF' kako biste popravili datoteku prije nastavka ove operacije. +error.pdfCorruptedMultiple=Jedna ili više PDF datoteka čini se oštećenima. Pokušajte prvo popraviti svaku datoteku pomoću značajke 'Popravi PDF' prije nego što ih pokušate spojiti. +error.pdfCorruptedDuring=Greška {0}: Čini se da je PDF datoteka oštećena. Pokušajte prvo upotrijebiti značajku 'Popravi PDF' kako biste popravili datoteku prije nastavka ove operacije. # Frontend corruption error messages -error.pdfInvalid=The PDF file "{0}" appears to be corrupted or has an invalid structure. Please try using the 'Repair PDF' feature to fix the file before proceeding. -error.tryRepair=Try using the Repair PDF feature to fix corrupted files. +error.pdfInvalid=Čini se da je PDF datoteka "{0}" oštećena ili ima neispravnu strukturu. Pokušajte upotrijebiti značajku 'Popravi PDF' kako biste popravili datoteku prije nastavka. +error.tryRepair=Pokušajte upotrijebiti značajku 'Popravi PDF' za popravak oštećenih datoteka. # Additional error messages -error.pdfEncryption=The PDF appears to have corrupted encryption data. This can happen when the PDF was created with incompatible encryption methods. Please try using the 'Repair PDF' feature first, or contact the document creator for a new copy. -error.fileProcessing=An error occurred while processing the file during {0} operation: {1} +error.pdfEncryption=Čini se da PDF ima oštećene podatke o enkripciji. To se može dogoditi kada je PDF stvoren nekompatibilnim metodama enkripcije. Pokušajte prvo upotrijebiti značajku 'Popravi PDF' ili se obratite autoru dokumenta za novu kopiju. +error.fileProcessing=Došlo je do pogreške prilikom obrade datoteke tijekom operacije {0}: {1} # Generic error message templates -error.toolNotInstalled={0} is not installed -error.toolRequired={0} is required for {1} -error.conversionFailed={0} conversion failed -error.commandFailed={0} command failed -error.algorithmNotAvailable={0} algorithm not available -error.optionsNotSpecified={0} options are not specified -error.fileFormatRequired=File must be in {0} format -error.invalidFormat=Invalid {0} format: {1} -error.endpointDisabled=This endpoint has been disabled by the admin -error.urlNotReachable=URL is not reachable, please provide a valid URL +error.toolNotInstalled={0} nije instaliran +error.toolRequired={0} je potreban za {1} +error.conversionFailed=Pretvorba {0} nije uspjela +error.commandFailed=Naredba {0} nije uspjela +error.algorithmNotAvailable=Algoritam {0} nije dostupan +error.optionsNotSpecified=Opcije {0} nisu navedene +error.fileFormatRequired=Datoteka mora biti u formatu {0} +error.invalidFormat=Neispravan format {0}: {1} +error.endpointDisabled=Administrator je onemogućio ovu krajnju točku +error.urlNotReachable=URL nije dostupan, unesite valjan URL +error.invalidUrlFormat=Invalid URL format provided. The provided format is invalid. # DPI and image rendering messages - used by frontend for dynamic translation # Backend sends: [TRANSLATE:messageKey:arg1,arg2] English message # Frontend parses this and replaces with localized versions using these keys -error.dpiExceedsLimit=DPI value {0} exceeds maximum safe limit of {1}. High DPI values can cause memory issues and crashes. Please use a lower DPI value. -error.pageTooBigForDpi=PDF page {0} is too large to render at {1} DPI. Please try a lower DPI value (recommended: 150 or less). -error.pageTooBigExceedsArray=PDF page {0} is too large to render at {1} DPI. The resulting image would exceed Java's maximum array size. Please try a lower DPI value (recommended: 150 or less). -error.pageTooBigFor300Dpi=PDF page {0} is too large to render at 300 DPI. The resulting image would exceed Java's maximum array size. Please use a lower DPI value for PDF-to-image conversion. +error.dpiExceedsLimit=Vrijednost DPI {0} premašuje maksimalno sigurno ograničenje od {1}. Visoke vrijednosti DPI mogu uzrokovati probleme s memorijom i rušenja. Koristite nižu vrijednost DPI. +error.pageTooBigForDpi=PDF stranica {0} je prevelika za prikazivanje pri {1} DPI. Pokušajte s nižom vrijednošću DPI (preporučeno: 150 ili manje). +error.pageTooBigExceedsArray=PDF stranica {0} je prevelika za prikazivanje pri {1} DPI. Rezultirajuća slika premašila bi maksimalnu veličinu polja u Javi. Pokušajte s nižom vrijednošću DPI (preporučeno: 150 ili manje). +error.pageTooBigFor300Dpi=PDF stranica {0} je prevelika za prikazivanje pri 300 DPI. Rezultirajuća slika premašila bi maksimalnu veličinu polja u Javi. Koristite nižu vrijednost DPI za pretvorbu PDF-a u sliku. # URL and website conversion messages # System requirements messages # Authentication and security messages -error.apiKeyInvalid=API key is not valid. -error.userNotFound=User not found. -error.passwordRequired=Password must not be null. -error.accountLocked=Your account has been locked due to too many failed login attempts. -error.invalidEmail=Invalid email addresses provided. -error.emailAttachmentRequired=An attachment is required to send the email. -error.signatureNotFound=Signature file not found. +error.apiKeyInvalid=API ključ nije valjan. +error.userNotFound=Korisnik nije pronađen. +error.passwordRequired=Lozinka ne smije biti prazna. +error.accountLocked=Vaš je račun zaključan zbog previše neuspjelih pokušaja prijave. +error.invalidEmail=Unesene su neispravne adrese e-pošte. +error.emailAttachmentRequired=Za slanje e-pošte potreban je privitak. +error.signatureNotFound=Datoteka s potpisom nije pronađena. # File processing messages -error.fileNotFound=File not found with ID: {0} +error.fileNotFound=Datoteka s ID-om nije pronađena: {0} # Database and configuration messages -error.noBackupScripts=No backup scripts were found. -error.unsupportedProvider={0} is not currently supported. -error.pathTraversalDetected=Path traversal detected for security reasons. +error.noBackupScripts=Nisu pronađene sigurnosne kopije. +error.unsupportedProvider={0} trenutno nije podržan. +error.pathTraversalDetected=Otkriven je pokušaj prelaska putanje iz sigurnosnih razloga. # Validation messages -error.invalidArgument=Invalid argument: {0} -error.argumentRequired={0} must not be null -error.operationFailed=Operation failed: {0} -error.angleNotMultipleOf90=Angle must be a multiple of 90 -error.pdfBookmarksNotFound=No PDF bookmarks/outline found in document -error.fontLoadingFailed=Error processing font file -error.fontDirectoryReadFailed=Failed to read font directory +error.invalidArgument=Neispravan argument: {0} +error.argumentRequired={0} ne smije biti prazno +error.operationFailed=Operacija nije uspjela: {0} +error.angleNotMultipleOf90=Kut mora biti višekratnik od 90 +error.pdfBookmarksNotFound=U dokumentu nisu pronađene PDF oznake/sadržaj +error.fontLoadingFailed=Pogreška pri obradi datoteke fonta +error.fontDirectoryReadFailed=Čitanje direktorija fontova nije uspjelo delete=Izbriši username=Korisničko ime -password=Zaporka +password=Lozinka welcome=Dobrodošli property=Svojstvo -black=Crno -white=Bijelo -red=Crveno -green=Zeleno -blue=Plavo +black=Crna +white=Bijela +red=Crvena +green=Zelena +blue=Plava custom=Prilagođeno... poweredBy=Pokreće yes=Da no=Ne -changedCredsMessage=Podaci za prijavu uspješno promijenjeni! +changedCredsMessage=Vjerodajnice promijenjene! notAuthenticatedMessage=Korisnik nije autentificiran. userNotFoundMessage=Korisnik nije pronađen. -incorrectPasswordMessage=Kriva zaporka. -usernameExistsMessage=Korisničko ime već postoji -invalidUsernameMessage=Nevažeće korisničko ime, korisničko ime može sadržavati samo slova, brojke i sljedeće posebne znakove @._+- ili mora biti važeća adresa e-pošte. -invalidPasswordMessage=Lozinka ne smije biti prazna i ne smije počinjati ni završavati sa razmakom. -confirmPasswordErrorMessage=Nova lozinka i potvrda nove lozinke moraju biti identične. -deleteCurrentUserMessage=Nije moguće izbrisati trenutno prijavljenog korisnika. +incorrectPasswordMessage=Trenutna lozinka je netočna. +usernameExistsMessage=Novo korisničko ime već postoji. +invalidUsernameMessage=Neispravno korisničko ime. Korisničko ime može sadržavati samo slova, brojeve i sljedeće posebne znakove @._+- ili mora biti valjana adresa e-pošte. +invalidPasswordMessage=Lozinka ne smije biti prazna i ne smije imati razmake na početku ili kraju. +confirmPasswordErrorMessage=Nova lozinka i Potvrda nove lozinke moraju se podudarati. +deleteCurrentUserMessage=Ne može se izbrisati trenutno prijavljeni korisnik. deleteUsernameExistsMessage=Korisničko ime ne postoji i ne može se izbrisati. -downgradeCurrentUserMessage=Nije moguće vratiti unazad ulogu trenutnog korisnika +downgradeCurrentUserMessage=Nije moguće smanjiti ulogu trenutnog korisnika disabledCurrentUserMessage=Trenutni korisnik ne može biti onemogućen -downgradeCurrentUserLongMessage=Nije moguće vratiti unazad ulogu trenutnog korisnika. Dakle, trenutni korisnik neće biti prikazan. +downgradeCurrentUserLongMessage=Nije moguće smanjiti ulogu trenutnog korisnika. Stoga trenutni korisnik neće biti prikazan. userAlreadyExistsOAuthMessage=Korisnik već postoji kao OAuth2 korisnik. userAlreadyExistsWebMessage=Korisnik već postoji kao web korisnik. -invalidRoleMessage=Invalid role. +invalidRoleMessage=Nevažeća uloga. error=Greška oops=Ups! help=Pomoć goHomepage=Idi na početnu stranicu -joinDiscord=Pridruži se našem Discord serveru -seeDockerHub=Vidi Docker Hub -visitGithub=Posjeti Github Repository +joinDiscord=Pridružite se našem Discord poslužitelju +seeDockerHub=Pogledajte na Docker Hubu +visitGithub=Posjetite Github repozitorij donate=Doniraj color=Boja sponsor=Sponzor info=Informacije pro=Pro -proFeatures=Pro Features +proFeatures=Pro značajke page=Stranica pages=Stranice loading=Učitavanje... addToDoc=Dodaj u dokument -reset=Reset -apply=Apply -noFileSelected=No file selected. Please upload one. -view=View -cancel=Cancel +reset=Poništi +apply=Primijeni +noFileSelected=Nije odabrana datoteka. Molimo učitajte jednu. +view=Prikaz +cancel=Odustani -back.toSettings=Back to Settings -back.toHome=Back to Home -back.toAdmin=Back to Admin +back.toSettings=Natrag na postavke +back.toHome=Natrag na početnu stranicu +back.toAdmin=Natrag na administraciju -legal.privacy=Politika privatnosti -legal.terms=Uspe sodržine -legal.accessibility=Dostupnost -legal.cookie=Politika kolačića -legal.impressum=Vedro ishoda -legal.showCookieBanner=Cookie Preferences +legal.privacy=Pravila o privatnosti +legal.terms=Uvjeti korištenja +legal.accessibility=Pristupačnost +legal.cookie=Pravila o kolačićima +legal.impressum=Impresum +legal.showCookieBanner=Postavke kolačića ############### # Pipeline # ############### -pipeline.header=Pipeline Meni (Beta) -pipeline.uploadButton=Prenesi prilagođeno -pipeline.configureButton=Konfigurirati +pipeline.header=Izbornik automatizacije (Beta) +pipeline.uploadButton=Učitaj prilagođeno +pipeline.configureButton=Konfiguriraj pipeline.defaultOption=Prilagođeno pipeline.submitButton=Pošalji -pipeline.help=Pipeline Pomoć +pipeline.help=Pomoć za automatizaciju pipeline.scanHelp=Pomoć za skeniranje mapa -pipeline.deletePrompt=Jeste li sigurni da želite obrisati pipeline? +pipeline.deletePrompt=Jeste li sigurni da želite izbrisati ovu automatizaciju? ###################### # Pipeline Options # ###################### -pipelineOptions.header=Pipeline Konfiguracija -pipelineOptions.pipelineNameLabel=Pipeline Ime -pipelineOptions.saveSettings=Spremi Postavke -pipelineOptions.pipelineNamePrompt=Unesite naziv pipeline-a ovdje -pipelineOptions.selectOperation=Odaberite Operaciju -pipelineOptions.addOperationButton=Dodajte operaciju -pipelineOptions.pipelineHeader=Pipeline: -pipelineOptions.saveButton=Preuzmi datoteku -pipelineOptions.validateButton=Potvrdi +pipelineOptions.header=Konfiguracija automatizacije +pipelineOptions.pipelineNameLabel=Naziv automatizacije +pipelineOptions.saveSettings=Spremi postavke operacije +pipelineOptions.pipelineNamePrompt=Unesite naziv automatizacije +pipelineOptions.selectOperation=Odaberite operaciju +pipelineOptions.addOperationButton=Dodaj operaciju +pipelineOptions.pipelineHeader=Automatizacija: +pipelineOptions.saveButton=Preuzmi +pipelineOptions.validateButton=Provjeri ######################## # ENTERPRISE EDITION # ######################## -enterpriseEdition.button=Ažurirajte na Pro -enterpriseEdition.warning=Ova funkcija je dostupna samo pro korisnicima. -enterpriseEdition.yamlAdvert=Stirling PDF Pro podrzava konfiguiracione datoteke u formati YAML i druga osobine SSO. -enterpriseEdition.ssoAdvert=Tražite još funkcija za upravljanje korisnicima? Razmotrite Stirling PDF Pro -enterpriseEdition.proTeamFeatureDisabled=Team management features require a Pro licence or higher +enterpriseEdition.button=Nadogradi na Pro +enterpriseEdition.warning=Ova značajka dostupna je samo Pro korisnicima. +enterpriseEdition.yamlAdvert=Stirling PDF Pro podržava YAML konfiguracijske datoteke i druge SSO značajke. +enterpriseEdition.ssoAdvert=Tražite više značajki za upravljanje korisnicima? Pogledajte Stirling PDF Pro +enterpriseEdition.proTeamFeatureDisabled=Značajke upravljanja timom zahtijevaju Pro licencu ili višu ################# # Analytics # ################# -analytics.title=Želite li da stvarate Stirling PDF bolji? -analytics.paragraph1=Stirling PDF ima uključene analitike koje nam pomažu da proizvod poboljšamo. Niste pratili nikakva osobna informacija ni sadržaj datoteka. -analytics.paragraph2=Razmotrite omogućivanje analitičkih podataka kako biste stvorili Stirling-PDF veće i da bismo bolje razumeli naših korisnika. -analytics.enable=Omogući analitike -analytics.disable=Onemogući analitike -analytics.settings=Možete promijeniti postavke za analitike u datoteci config/settings.yml +analytics.title=Želite li učiniti Stirling PDF boljim? +analytics.paragraph1=Stirling PDF ima opcionalnu analitiku koja nam pomaže poboljšati proizvod. Ne pratimo nikakve osobne podatke niti sadržaj datoteka. +analytics.paragraph2=Molimo razmislite o omogućavanju analitike kako biste pomogli Stirling PDF-u da raste i omogućili nam da bolje razumijemo naše korisnike. +analytics.enable=Omogući analitiku +analytics.disable=Onemogući analitiku +analytics.settings=Postavke za analitiku možete promijeniti u datoteci config/settings.yml ############# # NAVBAR # ############# navbar.favorite=Favoriti -navbar.recent=New and recently updated -navbar.darkmode=Tamni Način Rada +navbar.recent=Novo i nedavno ažurirano +navbar.darkmode=Tamni način rada navbar.language=Jezici navbar.settings=Postavke navbar.allTools=Alati -navbar.multiTool=Multi Tools (Alati) -navbar.search=Search -navbar.sections.organize=Organizirati +navbar.multiTool=Višenamjenski alat +navbar.search=Pretraži +navbar.sections.organize=Organiziraj navbar.sections.convertTo=Pretvori u PDF -navbar.sections.convertFrom=Pretvori iz PDF -navbar.sections.security=Potpis & sigurnost +navbar.sections.convertFrom=Pretvori iz PDF-a +navbar.sections.security=Potpis i sigurnost navbar.sections.advance=Napredno -navbar.sections.edit=Pregled & Uređivanje +navbar.sections.edit=Prikaz i uređivanje navbar.sections.popular=Popularno ############# @@ -368,514 +370,536 @@ settings.update=Dostupno ažuriranje settings.updateAvailable={0} je trenutno instalirana verzija. Dostupna je nova verzija ({1}). # Update modal and notification strings -update.urgentUpdateAvailable=🚨 Update Available -update.updateAvailable=Update Available -update.modalTitle=Update Available -update.current=Current -update.latest=Latest -update.latestStable=Latest Stable -update.priority=Priority -update.recommendedAction=Recommended Action -update.breakingChangesDetected=⚠️ Breaking Changes Detected -update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. -update.migrationGuides=Migration Guides: -update.viewGuide=View Guide -update.loadingDetailedInfo=Loading detailed version information... -update.close=Close -update.viewAllReleases=View All Releases -update.downloadLatest=Download Latest -update.availableUpdates=Available Updates: -update.unableToLoadDetails=Unable to load detailed version information. -update.version=Version +update.urgentUpdateAvailable=🚨 Dostupno je hitno ažuriranje +update.updateAvailable=Dostupno je ažuriranje +update.modalTitle=Dostupno je ažuriranje +update.current=Trenutna +update.latest=Najnovija +update.latestStable=Najnovija stabilna +update.priority=Prioritet +update.recommendedAction=Preporučena radnja +update.breakingChangesDetected=⚠️ Otkrivene su promjene koje mogu uzrokovati probleme +update.breakingChangesMessage=Ovo ažuriranje sadrži promjene koje mogu uzrokovati probleme. Molimo pregledajte vodiče za migraciju u nastavku. +update.migrationGuides=Vodiči za migraciju: +update.viewGuide=Prikaži vodič +update.loadingDetailedInfo=Učitavanje detaljnih informacija o verziji... +update.close=Zatvori +update.viewAllReleases=Prikaži sva izdanja +update.downloadLatest=Preuzmi najnovije +update.availableUpdates=Dostupna ažuriranja: +update.unableToLoadDetails=Nije moguće učitati detaljne informacije o verziji. +update.version=Verzija # Update priority levels -update.priority.urgent=URGENT -update.priority.normal=NORMAL -update.priority.minor=MINOR -update.priority.low=LOW +update.priority.urgent=HITNO +update.priority.normal=NORMALNO +update.priority.minor=MANJE +update.priority.low=NISKO # Breaking changes text -update.breakingChanges=Breaking Changes: -update.breakingChangesDefault=This version contains breaking changes -update.migrationGuide=Migration Guide +update.breakingChanges=Promjene koje mogu uzrokovati probleme: +update.breakingChangesDefault=Ova verzija sadrži promjene koje mogu uzrokovati probleme +update.migrationGuide=Vodič za migraciju settings.appVersion=Verzija aplikacije: -settings.downloadOption.title=Odaberite opciju preuzimanja (Za preuzimanje pojedinačnih datoteka bez zip formata): +settings.downloadOption.title=Odaberite opciju preuzimanja (za preuzimanje pojedinačnih datoteka koje nisu zip): settings.downloadOption.1=Otvori u istom prozoru settings.downloadOption.2=Otvori u novom prozoru settings.downloadOption.3=Preuzmi datoteku -settings.zipThreshold=Spremi .zip datoteku kada broj preuzetih datoteka pređe +settings.zipThreshold=Komprimiraj datoteke kada broj preuzetih datoteka premaši settings.signOut=Odjava settings.accountSettings=Postavke računa -settings.bored.help=Omogućuje "easter egg" igru -settings.cacheInputs.name=Spremi unose obrazaca -settings.cacheInputs.help=omogućiti pohranjivanje prethodno korištenih ulaza za buduća izvođenja +settings.bored.help=Omogućuje uskrsno jaje (igru) +settings.cacheInputs.name=Spremi unose obrasca +settings.cacheInputs.help=Omogućite za spremanje prethodno korištenih unosa za buduće pokretanje -changeCreds.title=Promijeni pristupne podatke -changeCreds.header=Ažurirajte korisničke podatke -changeCreds.changePassword=Koristite zadanu lozinku za prijavu. Unesite novu lozinku +changeCreds.title=Promijeni vjerodajnice +changeCreds.header=Ažurirajte podatke o svom računu +changeCreds.changePassword=Koristite zadane vjerodajnice za prijavu. Molimo unesite novu lozinku. changeCreds.newUsername=Novo korisničko ime -changeCreds.oldPassword=Trenutna zaporka -changeCreds.newPassword=Nova zaporka -changeCreds.confirmNewPassword=Potvrdite novu lozinku -changeCreds.submit=Potvrdi +changeCreds.oldPassword=Trenutna lozinka +changeCreds.newPassword=Nova lozinka +changeCreds.confirmNewPassword=Potvrdi novu lozinku +changeCreds.submit=Pošalji promjene account.title=Postavke računa account.accountSettings=Postavke računa -account.adminSettings=Admin Postavka - Pregled i dodavanje korisnika +account.adminSettings=Administratorske postavke - Pregled i dodavanje korisnika account.userControlSettings=Postavke kontrole korisnika account.changeUsername=Promijeni korisničko ime account.newUsername=Novo korisničko ime -account.password=Potvrda lozinke -account.oldPassword=Stara zaporka -account.newPassword=Nova zaporka +account.password=Lozinka za potvrdu +account.oldPassword=Stara lozinka +account.newPassword=Nova lozinka account.changePassword=Promijeni lozinku account.confirmNewPassword=Potvrdi novu lozinku account.signOut=Odjava -account.yourApiKey=Tvoj API ključ -account.syncTitle=Sinkronizirajte postavke preglednika s računom +account.yourApiKey=Vaš API ključ +account.syncTitle=Sinkroniziraj postavke preglednika s računom account.settingsCompare=Usporedba postavki: account.property=Svojstvo -account.webBrowserSettings=Postavka web-preglednika -account.syncToBrowser=Sinkronizacija Račun -> Preglednik -account.syncToAccount=Sinkronizacija Račun <- Preglednik -account.adminTitle=Administrator Tools -account.adminNotif=You have admin privileges. Access system settings and user management. +account.webBrowserSettings=Postavke web preglednika +account.syncToBrowser=Sinkroniziraj račun -> Preglednik +account.syncToAccount=Sinkroniziraj račun <- Preglednik +account.adminTitle=Administratorski alati +account.adminNotif=Imate administratorske ovlasti. Pristupite postavkama sustava i upravljanju korisnicima. -adminUserSettings.title=Postavka kontrole korisnika -adminUserSettings.header=Postavka kontrole korisnika za administratora +adminUserSettings.title=Postavke kontrole korisnika +adminUserSettings.header=Administratorske postavke kontrole korisnika adminUserSettings.admin=Administrator adminUserSettings.user=Korisnik adminUserSettings.addUser=Dodaj novog korisnika -adminUserSettings.deleteUser=Obriši korisnika -adminUserSettings.confirmDeleteUser=Treba li obračunati ovaj korisnika? -adminUserSettings.confirmChangeUserStatus=Treba li isključiti/uključiti ovog korisnika? -adminUserSettings.usernameInfo=Korisničko ime može sadržavati samo slova, brojke i sljedeće posebne znakove @._+- ili mora biti važeća adresa e-pošte. +adminUserSettings.deleteUser=Izbriši korisnika +adminUserSettings.confirmDeleteUser=Želite li izbrisati korisnika? +adminUserSettings.confirmChangeUserStatus=Želite li onemogućiti/omogućiti korisnika? +adminUserSettings.usernameInfo=Korisničko ime može sadržavati samo slova, brojeve i sljedeće posebne znakove @._+- ili mora biti valjana adresa e-pošte. adminUserSettings.role=Uloga adminUserSettings.actions=Akcije -adminUserSettings.apiUser=Korisnik s ograničenim API pristupom -adminUserSettings.extraApiUser=Dodatni korisnik s ograničenim API pristupom -adminUserSettings.webOnlyUser=Web Korisnik -adminUserSettings.demoUser=Demo korisnik (Bez prilagođenih Postavki) -adminUserSettings.internalApiUser=Interni API Korisnik -adminUserSettings.forceChange=Prisiliti korisnika da promijeni lozinku prilikom prijave +adminUserSettings.apiUser=Ograničeni API korisnik +adminUserSettings.extraApiUser=Dodatni ograničeni API korisnik +adminUserSettings.webOnlyUser=Samo web korisnik +adminUserSettings.demoUser=Demo korisnik (bez prilagođenih postavki) +adminUserSettings.internalApiUser=Interni API korisnik +adminUserSettings.forceChange=Prisili korisnika da promijeni lozinku pri prijavi adminUserSettings.submit=Spremi korisnika -adminUserSettings.changeUserRole=Promijenite korisničku ulogu -adminUserSettings.authenticated=Autentificirano -adminUserSettings.editOwnProfil=Uredi vlastit profil -adminUserSettings.enabledUser=Omotljiv korisnik -adminUserSettings.disabledUser=Onemogućen korisnik +adminUserSettings.changeUserRole=Promijeni ulogu korisnika +adminUserSettings.authenticated=Autentificiran +adminUserSettings.editOwnProfil=Uredi vlastiti profil +adminUserSettings.enabledUser=omogućen korisnik +adminUserSettings.disabledUser=onemogućen korisnik adminUserSettings.activeUsers=Aktivni korisnici: -adminUserSettings.disabledUsers=Isključeni korisnici: -adminUserSettings.totalUsers=Ukupan broj korisnika: +adminUserSettings.disabledUsers=Onemogućeni korisnici: +adminUserSettings.totalUsers=Ukupno korisnika: adminUserSettings.lastRequest=Zadnji zahtjev -adminUserSettings.usage=View Usage -adminUserSettings.teams=View/Edit Teams -adminUserSettings.team=Team -adminUserSettings.manageTeams=Manage Teams -adminUserSettings.createTeam=Create Team -adminUserSettings.viewTeam=View Team -adminUserSettings.deleteTeam=Delete Team -adminUserSettings.teamName=Team Name -adminUserSettings.teamExists=Team already exists -adminUserSettings.teamCreated=Team created successfully -adminUserSettings.teamChanged=User's team was updated -adminUserSettings.teamHidden=Hidden -adminUserSettings.totalMembers=Total Members -adminUserSettings.confirmDeleteTeam=Are you sure you want to delete this team? +adminUserSettings.usage=Prikaži korištenje +adminUserSettings.teams=Prikaži/Uredi timove +adminUserSettings.team=Tim +adminUserSettings.manageTeams=Upravljaj timovima +adminUserSettings.createTeam=Kreiraj tim +adminUserSettings.viewTeam=Prikaži tim +adminUserSettings.deleteTeam=Izbriši tim +adminUserSettings.teamName=Ime tima +adminUserSettings.teamExists=Tim već postoji +adminUserSettings.teamCreated=Tim uspješno kreiran +adminUserSettings.teamChanged=Tim korisnika je ažuriran +adminUserSettings.teamHidden=Skriveno +adminUserSettings.totalMembers=Ukupno članova +adminUserSettings.confirmDeleteTeam=Jeste li sigurni da želite izbrisati ovaj tim? -teamCreated=Team created successfully -teamExists=A team with that name already exists -teamNameExists=Another team with that name already exists -teamNotFound=Team not found -teamDeleted=Team deleted -teamHasUsers=Cannot delete a team with users assigned -teamRenamed=Team renamed successfully +teamCreated=Tim uspješno kreiran +teamExists=Tim s tim imenom već postoji +teamNameExists=Drugi tim s tim imenom već postoji +teamNotFound=Tim nije pronađen +teamDeleted=Tim izbrisan +teamHasUsers=Ne može se izbrisati tim s dodijeljenim korisnicima +teamRenamed=Tim uspješno preimenovan # Team user management -team.addUser=Add User to Team -team.selectUser=Select User -team.warning.moveUser=Warning: This will move the user from "{0}" team to "{1}" team. Are you sure? -team.confirm.moveUser=Are you sure you want to move this user from "{0}" team to "{1}" team? -team.userAdded=User successfully added to team -team.back=Back to Teams -team.internal=Internal Team -team.internalTeamNotAccessible=The Internal team is a system team and cannot be accessed -team.cannotMoveInternalUsers=Users in the Internal team cannot be moved to other teams -team.hidden=Hidden -team.name=Team Name -team.totalMembers=Total Members -team.members=Members -team.username=Username -team.role=Role +team.addUser=Dodaj korisnika u tim +team.selectUser=Odaberi korisnika +team.warning.moveUser=Upozorenje: Ovo će premjestiti korisnika iz tima "{0}" u tim "{1}". Jeste li sigurni? +team.confirm.moveUser=Jeste li sigurni da želite premjestiti ovog korisnika iz tima "{0}" u tim "{1}"? +team.userAdded=Korisnik uspješno dodan u tim +team.back=Natrag na timove +team.internal=Interni tim +team.internalTeamNotAccessible=Interni tim je sistemski tim i ne može mu se pristupiti +team.cannotMoveInternalUsers=Korisnici u internom timu ne mogu se premještati u druge timove +team.hidden=Skriveno +team.name=Ime tima +team.totalMembers=Ukupno članova +team.members=Članovi +team.username=Korisničko ime +team.role=Uloga team.status=Status -team.enabled=Enabled -team.disabled=Disabled -team.noMembers=This team has no members yet. +team.enabled=Omogućen +team.disabled=Onemogućen +team.noMembers=Ovaj tim još nema članova. -endpointStatistics.title=Endpoint Statistics -endpointStatistics.header=Endpoint Statistics +endpointStatistics.title=Statistika krajnjih točaka +endpointStatistics.header=Statistika krajnjih točaka endpointStatistics.top10=Top 10 endpointStatistics.top20=Top 20 -endpointStatistics.all=All -endpointStatistics.refresh=Refresh -endpointStatistics.includeHomepage=Include Homepage ('/') -endpointStatistics.includeLoginPage=Include Login Page ('/login') -endpointStatistics.totalEndpoints=Total Endpoints -endpointStatistics.totalVisits=Total Visits -endpointStatistics.showing=Showing -endpointStatistics.selectedVisits=Selected Visits -endpointStatistics.endpoint=Endpoint -endpointStatistics.visits=Visits -endpointStatistics.percentage=Percentage -endpointStatistics.loading=Loading... -endpointStatistics.failedToLoad=Failed to load endpoint data. Please try refreshing. -endpointStatistics.home=Home -endpointStatistics.login=Login -endpointStatistics.top=Top -endpointStatistics.numberOfVisits=Number of Visits -endpointStatistics.visitsTooltip=Visits: {0} ({1}% of total) -endpointStatistics.retry=Retry +endpointStatistics.all=Sve +endpointStatistics.refresh=Osvježi +endpointStatistics.includeHomepage=Uključi početnu stranicu ('/') +endpointStatistics.includeLoginPage=Uključi stranicu za prijavu ('/login') +endpointStatistics.totalEndpoints=Ukupno krajnjih točaka +endpointStatistics.totalVisits=Ukupno posjeta +endpointStatistics.showing=Prikazano +endpointStatistics.selectedVisits=Odabrane posjete +endpointStatistics.endpoint=Krajnja točka +endpointStatistics.visits=Posjete +endpointStatistics.percentage=Postotak +endpointStatistics.loading=Učitavanje... +endpointStatistics.failedToLoad=Neuspješno učitavanje podataka o krajnjim točkama. Molimo pokušajte osvježiti. +endpointStatistics.home=Početna +endpointStatistics.login=Prijava +endpointStatistics.top=Vrh +endpointStatistics.numberOfVisits=Broj posjeta +endpointStatistics.visitsTooltip=Posjete: {0} ({1}% od ukupnog) +endpointStatistics.retry=Pokušaj ponovo -database.title=Database Import/Export -database.header=Database Import/Export +database.title=Uvoz/izvoz baze podataka +database.header=Uvoz/izvoz baze podataka database.fileName=Ime datoteke database.creationDate=Datum stvaranja database.fileSize=Veličina datoteke -database.deleteBackupFile=Obriši zadao sažeto datoteke -database.importBackupFile=Uvezi sažeto datoteku -database.createBackupFile=Create Backup File -database.downloadBackupFile=Preuzmi sažeto datoteku -database.info_1=Kada uvažavate podatke, je ključno sigurno imati ispravan struktur. Ako niste sigurni šta uradite, tražite savjet i podršku od professionala. Greška u strukturi može uzrokovati greške u aplikaciji, do i uključujući potpunu nevjerojatnost funkcionalnosti aplikacije. -database.info_2=Ime datoteke nije relevantno prijevezi. Buduće bit će ponovno oznaceno za određeni format backup_user_yyyyMMddHHmm.sql, čime se osigurava konzistentna nazivnica. -database.submit=Uvezi sažeto -database.importIntoDatabaseSuccessed=Uvez u bazu podataka uspio -database.backupCreated=Database backup successful -database.fileNotFound=File not Found -database.fileNullOrEmpty=Datoteka ne smije biti null ili prazna -database.failedImportFile=Failed Import File -database.notSupported=This function is not available for your database connection. +database.deleteBackupFile=Izbriši sigurnosnu kopiju +database.importBackupFile=Uvezi sigurnosnu kopiju +database.createBackupFile=Kreiraj sigurnosnu kopiju +database.downloadBackupFile=Preuzmi sigurnosnu kopiju +database.info_1=Pri uvozu podataka ključno je osigurati ispravnu strukturu. Ako niste sigurni što radite, potražite savjet i podršku stručnjaka. Pogreška u strukturi može uzrokovati kvarove aplikacije, pa čak i potpunu nemogućnost pokretanja aplikacije. +database.info_2=Ime datoteke pri učitavanju nije bitno. Nakon toga će biti preimenovana kako bi slijedila format backup_user_yyyyMMddHHmm.sql, osiguravajući dosljednu konvenciju imenovanja. +database.submit=Uvezi sigurnosnu kopiju +database.importIntoDatabaseSuccessed=Uvoz u bazu podataka uspio +database.backupCreated=Sigurnosna kopija baze podataka uspješna +database.fileNotFound=Datoteka nije pronađena +database.fileNullOrEmpty=Datoteka ne smije biti prazna +database.failedImportFile=Neuspješan uvoz datoteke +database.notSupported=Ova funkcija nije dostupna za vašu vezu s bazom podataka. -session.expired=Vaš sesija je istekla. Molim vas da osvježite stranicu i pokušate ponovno. -session.refreshPage=Refresh Page +session.expired=Vaša sesija je istekla. Molimo osvježite stranicu i pokušajte ponovo. +session.refreshPage=Osvježi stranicu ############# # HOME-PAGE # ############# -home.desc=Sve na jednom mjestu za sve vaše PDF potrebe. -home.searchBar=Pretraži funkcije... +home.desc=Vaše lokalno rješenje na jednom mjestu za sve vaše PDF potrebe. +home.searchBar=Pretražite značajke... -home.viewPdf.title=View/Edit PDF -home.viewPdf.desc=Pregledaj, komentiraj, dodaj tekst ili slike -viewPdf.tags=pregled,čitanje,komentiranje,tekst,slika +home.viewPdf.title=Pregled/Uređivanje PDF-a +home.viewPdf.desc=Pregledajte, dodajte bilješke, crtajte, dodajte tekst ili slike +viewPdf.tags=pregled,čitanje,bilješke,tekst,slika,isticanje,uređivanje -home.setFavorites=Set Favourites -home.hideFavorites=Hide Favourites -home.showFavorites=Show Favourites -home.legacyHomepage=Old homepage -home.newHomePage=Try our new homepage! -home.alphabetical=Alphabetical -home.globalPopularity=Global Popularity -home.sortBy=Sort by: +home.setFavorites=Postavi favorite +home.hideFavorites=Sakrij favorite +home.showFavorites=Prikaži favorite +home.legacyHomepage=Stara početna stranica +home.newHomePage=Isprobajte našu novu početnu stranicu! +home.alphabetical=Abecedno +home.globalPopularity=Globalna popularnost +home.sortBy=Sortiraj po: -home.multiTool.title=PDF Višestruki alat -home.multiTool.desc=Spajanje, rotiranje, preuređivanje i uklanjanje stranica -multiTool.tags=Višestruki alat, više operacija, korisničko sučelje, povlačenje klikom, prednji kraj, strana klijenta, interaktivno, nepopravljivo, pomicanje +home.multiTool.title=PDF višenamjenski alat +home.multiTool.desc=Spajanje, rotiranje, preuređivanje, razdvajanje i uklanjanje stranica +multiTool.tags=Višenamjenski alat,Više operacija,UI,klikni i povuci,klijentska strana,interaktivno,pomakni,izbriši,migriraj,podijeli home.merge.title=Spajanje home.merge.desc=Jednostavno spojite više PDF-ova u jedan. -merge.tags=spajanje,Operacije sa stranicama,Backend,poslužiteljska strana +merge.tags=spajanje,operacije sa stranicama,poslužiteljska strana home.split.title=Razdvajanje home.split.desc=Razdvojite PDF-ove u više dokumenata -split.tags=Operacije stranice, dijeljenje, više stranica, rezanje,poslužiteljska strana +split.tags=operacije sa stranicama,dijeljenje,više stranica,rezanje,poslužiteljska strana -home.rotate.title=Rotacija -home.rotate.desc=Jednostavno rotirajte vaše PDF-ove. +home.rotate.title=Rotiranje +home.rotate.desc=Jednostavno rotirajte svoje PDF-ove. rotate.tags=poslužiteljska strana home.imageToPdf.title=Slika u PDF -home.imageToPdf.desc=Pretvorite sliku (PNG, JPEG, GIF) u PDF. -imageToPdf.tags=konverzija,pretvaranje,img,jpg,slika,foto +home.imageToPdf.desc=Pretvorite sliku (PNG, JPEG, GIF, PSD) u PDF. +imageToPdf.tags=pretvorba,img,jpg,slika,fotografija,psd,photoshop -home.pdfToImage.title=PDF u Sliku -home.pdfToImage.desc=Pretvorite PDF u sliku. (PNG, JPEG, GIF) -pdfToImage.tags=konverzija,img,jpg,slika,foto +home.cbzToPdf.title=CBZ to PDF +home.cbzToPdf.desc=Convert CBZ comic book archives to PDF format. +cbzToPdf.tags=conversion,comic,book,archive,cbz,zip + +home.cbrToPdf.title=CBR to PDF +home.cbrToPdf.desc=Convert CBR comic book archives to PDF format. +cbrToPdf.tags=conversion,comic,book,archive,cbr,rar + +home.pdfToCbz.title=PDF to CBZ +home.pdfToCbz.desc=Convert PDF files to CBZ comic book archives. +pdfToCbz.tags=conversion,comic,book,archive,cbz,pdf + +home.pdfToCbr.title=PDF to CBR +home.pdfToCbr.desc=Convert PDF files to CBR comic book archives. +pdfToCbr.tags=conversion,comic,book,archive,cbr,rar + +home.pdfToImage.title=PDF u sliku +home.pdfToImage.desc=Pretvorite PDF u sliku (PNG, JPEG, GIF, PSD). +pdfToImage.tags=pretvorba,img,jpg,slika,fotografija,psd,photoshop home.pdfOrganiser.title=Organiziranje -home.pdfOrganiser.desc=Uklonite/preuredite stranice bilo kojim redoslijedom -pdfOrganiser.tags=dvostrana,parne,neparni,prikupljanje,prebacivanje +home.pdfOrganiser.desc=Uklonite/preuredite stranice u bilo kojem redoslijedu +pdfOrganiser.tags=dvostrano,parno,neparno,sortiranje,pomicanje home.addImage.title=Dodaj sliku -home.addImage.desc=Dodaje sliku na zadano mjesto u PDF-u -addImage.tags=img,jpg,slika,foto +home.addImage.desc=Dodaje sliku na određeno mjesto u PDF-u +addImage.tags=img,jpg,slika,fotografija -home.attachments.title=Add Attachments -home.attachments.desc=Add or remove embedded files (attachments) to/from a PDF -attachments.tags=embed,attach,file,attachment,attachments +home.attachments.title=Dodaj privitke +home.attachments.desc=Dodajte ili uklonite ugrađene datoteke (privitke) u/iz PDF-a +attachments.tags=ugradi,privitak,datoteka,privitci home.watermark.title=Dodaj vodeni žig -home.watermark.desc=DDodajte prilagođeni vodeni žig svom PDF dokumentu. -watermark.tags=Tekst,ponavljanje,etiketa,vlastiti,autorsko pravo,zaštita, img,jpg,slika,foto +home.watermark.desc=Dodajte prilagođeni vodeni žig u svoj PDF dokument. +watermark.tags=tekst,ponavljanje,oznaka,vlastiti,autorsko pravo,zaštitni znak,img,jpg,slika,fotografija -home.permissions.title=Promjena dopuštenja +home.permissions.title=Promijeni dopuštenja home.permissions.desc=Promijenite dopuštenja svog PDF dokumenta -permissions.tags=čitanje,pisanje,izmjena,print +permissions.tags=čitanje,pisanje,uređivanje,ispis -home.removePages.title=Ukloniti +home.removePages.title=Ukloni stranice home.removePages.desc=Izbrišite neželjene stranice iz svog PDF dokumenta. -removePages.tags=Ukloni stranice,izbriši stranice +removePages.tags=ukloni stranice,izbriši stranice home.addPassword.title=Dodaj lozinku -home.addPassword.desc=Šifrirajte svoj PDF dokument lozinkom.. -addPassword.tags=sigurno, sigurnost +home.addPassword.desc=Zaštitite svoj PDF dokument lozinkom. +addPassword.tags=sigurno,sigurnost home.removePassword.title=Ukloni lozinku -home.removePassword.desc=Uklonite zaštitu lozinkom sa svog PDF dokumenta.. -removePassword.tags=sigurno, dešifriranje, sigurnost, poništi lozinku, izbriši lozinku +home.removePassword.desc=Uklonite zaštitu lozinkom iz svog PDF dokumenta. +removePassword.tags=sigurno,dešifriraj,sigurnost,ukloni lozinku,izbriši lozinku home.compressPdfs.title=Komprimiraj home.compressPdfs.desc=Komprimirajte PDF-ove kako biste smanjili njihovu veličinu. -compressPdfs.tags=squish, mali, maleni +compressPdfs.tags=stisni,mali,sitni -home.unlockPDFForms.title=Unlock PDF Forms -home.unlockPDFForms.desc=Remove read-only property of form fields in a PDF document. -unlockPDFForms.tags=remove,delete,form,field,readonly +home.unlockPDFForms.title=Otključaj PDF obrasce +home.unlockPDFForms.desc=Uklonite svojstvo samo za čitanje s polja obrasca u PDF dokumentu. +unlockPDFForms.tags=ukloni,izbriši,obrazac,polje,samo za čitanje -home.changeMetadata.title=Promjena metapodataka -home.changeMetadata.desc=Promjeni/Ukloni/Dodaj metapodatke iz PDF dokumenta -changeMetadata.tags=Naslov,autor,datum,kreacije,vrijeme,izdavač,proizvođač,statistike +home.changeMetadata.title=Promijeni metapodatke +home.changeMetadata.desc=Promijenite/uklonite/dodajte metapodatke iz PDF dokumenta +changeMetadata.tags=naslov,autor,datum,stvaranje,vrijeme,izdavač,proizvođač,statistika home.fileToPDF.title=Pretvori datoteku u PDF -home.fileToPDF.desc=Pretvorite gotovo sve datoteke u PDF (DOCX, PNG, XLS, PPT, TXT i više) -fileToPDF.tags=transformacija,format,dokument,slika,slajd,tekst,konverzija,office,docs,word,excel,powerpoint +home.fileToPDF.desc=Pretvorite gotovo bilo koju datoteku u PDF (DOCX, PNG, XLS, PPT, TXT i više) +fileToPDF.tags=transformacija,format,dokument,slika,slajd,tekst,pretvorba,office,docs,word,excel,powerpoint -home.ocr.title=OCR / Čišćenje skeniranih dokumenata -home.ocr.desc=Čišćenje skenira i otkriva tekst sa slika unutar PDF-a i ponovno ga dodaje kao tekst. -ocr.tags=prepoznavanje,tekst,slika,sken,čitanje,identifikacija,detektiranje,uređivanje +home.ocr.title=OCR / Čišćenje skenova +home.ocr.desc=Očistite skenove, prepoznajte tekst sa slika unutar PDF-a i ponovno ga dodajte kao tekst. +ocr.tags=prepoznavanje,tekst,slika,sken,čitanje,identificiranje,detekcija,uredivo -home.extractImages.title=Ekstrakt slika -home.extractImages.desc=Izdvaja sve slike iz PDF-a i sprema ih u zip format -extractImages.tags=slika, fotografija, spremanje, arhiva, zip, snimanje, zgrabi +home.extractImages.title=Izdvoji slike +home.extractImages.desc=Izdvaja sve slike iz PDF-a i sprema ih u zip arhivu +extractImages.tags=slika,fotografija,spremi,arhiva,zip,uhvati,zgrabi home.pdfToPDFA.title=PDF u PDF/A home.pdfToPDFA.desc=Pretvorite PDF u PDF/A za dugoročnu pohranu -pdfToPDFA.tags=arhiva,dugoročno,standardno,konverzija,čuvanje,čuvanje +pdfToPDFA.tags=arhiva,dugoročno,standard,pretvorba,pohrana,očuvanje home.PDFToWord.title=PDF u Word home.PDFToWord.desc=Pretvorite PDF u Word formate (DOC, DOCX i ODT) -PDFToWord.tags=doc,docx,odt,word,transformacija,format,konverzija,office,microsoft,docfile +PDFToWord.tags=doc,docx,odt,word,transformacija,format,pretvorba,office,microsoft,docfile -home.PDFToPresentation.title=PDF u Prezentaciju +home.PDFToPresentation.title=PDF u prezentaciju home.PDFToPresentation.desc=Pretvorite PDF u formate za prezentaciju (PPT, PPTX i ODP) PDFToPresentation.tags=slajdovi,prikaz,office,microsoft home.PDFToText.title=PDF u RTF (Tekst) -home.PDFToText.desc=Pretvorite PDF u tekst ili RTF format -PDFToText.tags=bojaformata,tjedentextformat,sadržanotekstformat +home.PDFToText.desc=Pretvorite PDF u tekstualni ili RTF format +PDFToText.tags=richformat,richtextformat,rich text format home.PDFToHTML.title=PDF u HTML home.PDFToHTML.desc=Pretvorite PDF u HTML format -PDFToHTML.tags=web sadržaj,prijateljski za pretraživače +PDFToHTML.tags=web sadržaj,prijateljski za preglednike home.PDFToXML.title=PDF u XML home.PDFToXML.desc=Pretvorite PDF u XML format -PDFToXML.tags=izdvajanje-podataka,strukturirani-sadržaj,interop,transformacija,konvertiranje +PDFToXML.tags=izdvajanje-podataka,strukturirani-sadržaj,interoperabilnost,transformacija,pretvorba home.ScannerImageSplit.title=Otkrij/razdvoji skenirane fotografije -home.ScannerImageSplit.desc=Razdvaja više fotografija iz fotografije/PDF-a -ScannerImageSplit.tags=razdvoji,auto-detekcija,skeniranja,višestruke fotografije,organizacija +home.ScannerImageSplit.desc=Razdvaja više fotografija unutar jedne fotografije/PDF-a +ScannerImageSplit.tags=odvoji,auto-detekcija,skenovi,više-fotografija,organiziraj -home.sign.title=Potpisati -home.sign.desc=Dodaje potpis u PDF crtežom, tekstom ili slikom -sign.tags=autorizacija,inicijali,crtani-potpis,tekstualni-potpis,slikovni-potpis +home.sign.title=Potpiši +home.sign.desc=Dodaje potpis u PDF crtanjem, tekstom ili slikom +sign.tags=autoriziraj,inicijali,crtani-potpis,tekstualni-potpis,slikovni-potpis -home.flatten.title=Ravnanje (Flatten) +home.flatten.title=Poravnaj home.flatten.desc=Uklonite sve interaktivne elemente i obrasce iz PDF-a -flatten.tags=statično,deaktivirati,neinteraktivno,usmjeriti +flatten.tags=statično,deaktiviraj,neinteraktivno,pojednostavi home.repair.title=Popravi -home.repair.desc=Pokušava popraviti oštećeni/pokvareni PDF -repair.tags=popravi,vrati,korekcija,obnovi +home.repair.desc=Pokušava popraviti oštećen/pokvaren PDF +repair.tags=popravi,vrati,ispravak,oporavi home.removeBlanks.title=Ukloni prazne stranice home.removeBlanks.desc=Otkriva i uklanja prazne stranice iz dokumenta -removeBlanks.tags=čišćenje,usmjeriti,ne-sadržaj,organizacija +removeBlanks.tags=čišćenje,pojednostavi,bez-sadržaja,organiziraj -home.removeAnnotations.title=Ukloni komentare -home.removeAnnotations.desc=Uklanja sve komentare/anotacije iz PDF-a +home.removeAnnotations.title=Ukloni bilješke +home.removeAnnotations.desc=Uklanja sve komentare/bilješke iz PDF-a removeAnnotations.tags=komentari,isticanje,bilješke,oznake,ukloni -home.compare.title=Uporedi -home.compare.desc=Uspoređuje i pokazuje razlike između 2 PDF dokumenta -compare.tags=razlikovati,kontrast,izmjene,analiza +home.compare.title=Usporedi +home.compare.desc=Uspoređuje i prikazuje razlike između 2 PDF dokumenta +compare.tags=razlikuj,usporedi,promjene,analiza -home.certSign.title=Potpišite s certifikatom -home.certSign.desc=Potpisuje PDF s certifikatom/ključem (PEM/P12) -certSign.tags=autentifikacija,PEM,P12,zvanično,šifriranje +home.certSign.title=Potpiši certifikatom +home.certSign.desc=Potpisuje PDF certifikatom/ključem (PEM/P12) +certSign.tags=autentificiraj,PEM,P12,službeno,šifriraj -home.removeCertSign.title=Ukloni potpis sertifikata -home.removeCertSign.desc=Uklonite potpis sertifikata iz PDF-a -removeCertSign.tags=autentičiranje,PEM,P12,djelomičan dešifriranje +home.removeCertSign.title=Ukloni potpis certifikata +home.removeCertSign.desc=Uklanja potpis certifikata iz PDF-a +removeCertSign.tags=autentificiraj,PEM,P12,službeno,dešifriraj -home.pageLayout.title=Izgled s više stranica +home.pageLayout.title=Raspored s više stranica home.pageLayout.desc=Spojite više stranica PDF dokumenta u jednu stranicu -pageLayout.tags=spajanje,kompozitni,pojedinačan-prikaz,organizacija +pageLayout.tags=spoji,kompozitno,jedan-prikaz,organiziraj -home.scalePages.title=Prilagodite veličinu/razmjer stranice -home.scalePages.desc=Promijenite veličinu/razmjer stranice i/ili njezin sadržaj. -scalePages.tags=izmjena,modifikacija,dimenzija,adaptacija +home.scalePages.title=Prilagodi veličinu/mjerilo stranice +home.scalePages.desc=Promijenite veličinu/mjerilo stranice i/ili njezinog sadržaja. +scalePages.tags=promijeni veličinu,izmijeni,dimenzija,prilagodi -home.pipeline.title=Pipeline -home.pipeline.desc=Izvršite više radnji na PDF-ovima definiranjem skripti u pipeline-u -pipeline.tags=automatizacija,sekvenciranje,skriptirano,batch-process +home.pipeline.title=Automatizacija +home.pipeline.desc=Pokrenite više radnji na PDF-ovima definiranjem skripti za automatizaciju +pipeline.tags=automatiziraj,sekvenca,skriptirano,skupna-obrada home.add-page-numbers.title=Dodaj brojeve stranica -home.add-page-numbers.desc=Dodajte brojeve stranica kroz dokument na određeno mjesto -add-page-numbers.tags=paginirati, označiti, organizirati, indeksirati +home.add-page-numbers.desc=Dodajte brojeve stranica na određeno mjesto u dokumentu +add-page-numbers.tags=paginiraj,označi,organiziraj,indeksiraj -home.auto-rename.title=Automatsko preimenovanje PDF datoteka -home.auto-rename.desc=Automatski preimenuje PDF datoteku na temelju otkrivenog zaglavlja -auto-rename.tags=auto-detekcija,zaglavlje-bazirano,organizacija,preimenovanje +home.auto-rename.title=Automatsko preimenovanje PDF datoteke +home.auto-rename.desc=Automatski preimenuje PDF datoteku na temelju njezinog prepoznatog zaglavlja +auto-rename.tags=auto-detekcija,na temelju zaglavlja,organiziraj,preimenuj home.adjust-contrast.title=Podesi boje/kontrast home.adjust-contrast.desc=Podesite kontrast, zasićenost i svjetlinu PDF-a -adjust-contrast.tags=korekcija boje, ugađanje, modificiranje, poboljšanje +adjust-contrast.tags=korekcija-boje,podesi,izmijeni,poboljšaj -home.crop.title=Izrežite PDF +home.crop.title=Izreži PDF home.crop.desc=Izrežite PDF kako biste smanjili njegovu veličinu (zadržava tekst!) -crop.tags=obrezivanje, smanjivanje, uređivanje, oblikovanje +crop.tags=obrezivanje,smanji,uredi,oblikuj home.autoSplitPDF.title=Automatsko dijeljenje stranica -home.autoSplitPDF.desc=Automatsko dijeljenje skeniranog PDF-a s fizičkim QR kodom za dijeljenje stranica -autoSplitPDF.tags=QR-bazirano,razdvoji,segment-skeniranja,organizacija +home.autoSplitPDF.desc=Automatski podijeli skenirani PDF pomoću fizičkog QR koda za dijeljenje stranica +autoSplitPDF.tags=na temelju QR-a,odvoji,segment-skeniranja,organiziraj -home.sanitizePdf.title=Dezinficirati (Sanitize) +home.sanitizePdf.title=Saniraj home.sanitizePdf.desc=Uklonite skripte i druge elemente iz PDF datoteka -sanitizePdf.tags=čisto, sigurno, sigurno, uklanjanje prijetnji +sanitizePdf.tags=očisti,osiguraj,sigurno,ukloni-prijetnje -home.URLToPDF.title=URL/Webstranica u PDF -home.URLToPDF.desc=Pretvara bilo koji http(s)URL u PDF -URLToPDF.tags=uhvati-web,sačuvaj-stranicu,web-u-doc,arhiva +home.URLToPDF.title=URL/Web stranica u PDF +home.URLToPDF.desc=Pretvara bilo koji http(s) URL u PDF +URLToPDF.tags=snimanje-weba,spremi-stranicu,web-u-dokument,arhiviraj home.HTMLToPDF.title=HTML u PDF -home.HTMLToPDF.desc=Pretvara bilo koji HTML datoteku ili zip u PDF -HTMLToPDF.tags=oznake,web-sadržaj,transformacija,konvertiranje +home.HTMLToPDF.desc=Pretvara bilo koju HTML datoteku ili zip arhivu u PDF +HTMLToPDF.tags=označavanje,web-sadržaj,transformacija,pretvorba #eml-to-pdf -home.EMLToPDF.title=Email to PDF -home.EMLToPDF.desc=Converts email (EML) files to PDF format including headers, body, and inline images -EMLToPDF.tags=email,conversion,eml,message,transformation,convert,mail +home.EMLToPDF.title=E-pošta u PDF +home.EMLToPDF.desc=Pretvara datoteke e-pošte (EML) u PDF format uključujući zaglavlja, tijelo i ugrađene slike +EMLToPDF.tags=e-pošta,pretvorba,eml,poruka,transformacija,pretvorba,pošta -EMLToPDF.title=Email To PDF -EMLToPDF.header=Email To PDF -EMLToPDF.submit=Convert -EMLToPDF.downloadHtml=Download HTML intermediate file instead of PDF -EMLToPDF.downloadHtmlHelp=This allows you to see the HTML version before PDF conversion and can help debug formatting issues -EMLToPDF.includeAttachments=Include attachments in PDF -EMLToPDF.maxAttachmentSize=Maximum attachment size (MB) -EMLToPDF.help=Converts email (EML) files to PDF format including headers, body, and inline images -EMLToPDF.troubleshootingTip1=Email to HTML is a more reliable process, so with batch-processing it is recommended to save both -EMLToPDF.troubleshootingTip2=With a small number of Emails, if the PDF is malformed, you can download HTML and override some of the problematic HTML/CSS code. -EMLToPDF.troubleshootingTip3=Embeddings, however, do not work with HTMLs +EMLToPDF.title=E-pošta u PDF +EMLToPDF.header=E-pošta u PDF +EMLToPDF.submit=Pretvori +EMLToPDF.downloadHtml=Preuzmi HTML među-datoteku umjesto PDF-a +EMLToPDF.downloadHtmlHelp=Ovo vam omogućuje da vidite HTML verziju prije pretvorbe u PDF i može pomoći u otklanjanju pogrešaka u formatiranju +EMLToPDF.includeAttachments=Uključi privitke u PDF +EMLToPDF.maxAttachmentSize=Maksimalna veličina privitka (MB) +EMLToPDF.help=Pretvara datoteke e-pošte (EML) u PDF format uključujući zaglavlja, tijelo i ugrađene slike +EMLToPDF.troubleshootingTip1=Pretvorba e-pošte u HTML je pouzdaniji proces, pa se kod skupne obrade preporučuje spremanje oba +EMLToPDF.troubleshootingTip2=S malim brojem e-poruka, ako je PDF neispravan, možete preuzeti HTML i nadjačati neke od problematičnih HTML/CSS kodova. +EMLToPDF.troubleshootingTip3=Međutim, ugrađivanja ne rade s HTML-om home.MarkdownToPDF.title=Markdown u PDF home.MarkdownToPDF.desc=Pretvara bilo koju Markdown datoteku u PDF -MarkdownToPDF.tags=oznake,web-sadržaj,transformacija,konvertiranje +MarkdownToPDF.tags=označavanje,web-sadržaj,transformacija,pretvorba,md -home.PDFToMarkdown.title=PDF to Markdown -home.PDFToMarkdown.desc=Converts any PDF to Markdown -PDFToMarkdown.tags=markup,web-content,transformation,convert,md +home.PDFToMarkdown.title=PDF u Markdown +home.PDFToMarkdown.desc=Pretvara bilo koji PDF u Markdown +PDFToMarkdown.tags=označavanje,web-sadržaj,transformacija,pretvorba,md home.getPdfInfo.title=Dohvati SVE informacije o PDF-u home.getPdfInfo.desc=Dohvaća sve moguće informacije o PDF-ovima -getPdfInfo.tags=informacije,podaci,statistike +getPdfInfo.tags=informacije,podaci,statistika,statistike home.extractPage.title=Izdvoji stranicu(e) home.extractPage.desc=Izdvaja odabrane stranice iz PDF-a -extractPage.tags=izdvajanje +extractPage.tags=izdvoji -home.PdfToSinglePage.title=PDF u Jednu Veliku Stranicu +home.PdfToSinglePage.title=Jedna velika stranica home.PdfToSinglePage.desc=Spaja sve PDF stranice u jednu veliku stranicu -PdfToSinglePage.tags=jedna-stranica +PdfToSinglePage.tags=jedna stranica -home.showJS.title=Prikaži JavaScript -home.showJS.desc=Pretražuje i prikazuje bilo koji JavaScript umetnut u PDF +home.showJS.title=Prikaži Javascript +home.showJS.desc=Pretražuje i prikazuje bilo koji JS umetnut u PDF showJS.tags=JS -home.autoRedact.title=Automatsko uređivanje -home.autoRedact.desc=Automatski redigira (zacrni) tekst u PDF-u na temelju unosa teksta -autoRedact.tags=Cenzura,Sakrij,prekrivanje,crna,marker,skriveno +home.autoRedact.title=Automatsko zatamnjivanje +home.autoRedact.desc=Automatski zatamnjuje tekst u PDF-u na temelju unesenog teksta +autoRedact.tags=zatamni,sakrij,zacrni,crno,marker,skriveno -home.redact.title=Manual Redaction -home.redact.desc=Redacts a PDF based on selected text, drawn shapes and/or selected page(s) -redact.tags=Redact,Hide,black out,black,marker,hidden,manual +home.redact.title=Ručno zatamnjivanje +home.redact.desc=Zatamnjuje PDF na temelju odabranog teksta, nacrtanih oblika i/ili odabranih stranica +redact.tags=zatamni,sakrij,zacrni,crno,marker,skriveno,ručno home.tableExtraxt.title=PDF u CSV -home.tableExtraxt.desc=Izdvaja tablice iz PDF-a pretvarajući ga u CSV -tableExtraxt.tags=CSV,Izdvajanje tabela,izdvajanje,pretvaranje +home.tableExtraxt.desc=Izdvaja tablice iz PDF-a i pretvara ih u CSV +tableExtraxt.tags=CSV,izdvajanje tablica,izdvoji,pretvorba home.autoSizeSplitPDF.title=Automatska podjela po veličini/broju home.autoSizeSplitPDF.desc=Podijelite jedan PDF na više dokumenata na temelju veličine, broja stranica ili broja dokumenata -autoSizeSplitPDF.tags=pdf,podjela,dokumenti,organizacija +autoSizeSplitPDF.tags=pdf,podjela,dokument,organizacija home.overlay-pdfs.title=Preklapanje PDF-ova -home.overlay-pdfs.desc=Preklapa PDF-ove na drugi PDF -overlay-pdfs.tags=Preklapanje +home.overlay-pdfs.desc=Preklapa PDF-ove jedan preko drugog +overlay-pdfs.tags=preklapanje home.split-by-sections.title=Podijeli PDF po odjeljcima home.split-by-sections.desc=Svaku stranicu PDF-a podijelite na manje vodoravne i okomite dijelove -split-by-sections.tags=Dijeljenje odjeljaka,Dijeljenje,Postavke +split-by-sections.tags=dijeljenje odjeljaka, podijeli, prilagodi home.AddStampRequest.title=Dodaj pečat u PDF -home.AddStampRequest.desc=Dodajte tekst ili dodajte slikovne oznake na postavljenim mjestima -AddStampRequest.tags=Pečat, dodavanje slike, središnja slika, vodeni žig, PDF, ugradnja, prilagodba +home.AddStampRequest.desc=Dodajte tekstualne ili slikovne pečate na određena mjesta +AddStampRequest.tags=pečat, dodaj sliku, centriraj sliku, vodeni žig, PDF, ugradi, prilagodi home.removeImagePdf.title=Ukloni sliku -home.removeImagePdf.desc=Ukloni sliku iz PDF-a kako bi se smanjio veličina datoteke -removeImagePdf.tags=Ukloni sliku, Rad sa stranicama, Back end, server strana +home.removeImagePdf.desc=Ukloni sliku iz PDF-a kako bi se smanjila veličina datoteke +removeImagePdf.tags=ukloni sliku,operacije sa stranicama,poslužiteljska strana -home.splitPdfByChapters.title=Podijeli PDF prema glavama -home.splitPdfByChapters.desc=Podijeli PDF na više datoteka prema njegovom strukturnom obliku glava. -splitPdfByChapters.tags=podjela, glave, markere, organizacija +home.splitPdfByChapters.title=Podijeli PDF po poglavljima +home.splitPdfByChapters.desc=Podijeli PDF na više datoteka na temelju njegove strukture poglavlja. +splitPdfByChapters.tags=podjela,poglavlja,oznake,organiziraj -home.validateSignature.title=Validate PDF Signature -home.validateSignature.desc=Verify digital signatures and certificates in PDF documents -validateSignature.tags=signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate +home.validateSignature.title=Provjeri PDF potpis +home.validateSignature.desc=Provjerite digitalne potpise i certifikate u PDF dokumentima +validateSignature.tags=potpis,provjeri,validiraj,pdf,certifikat,digitalni potpis,provjeri potpis,provjeri certifikat #replace-invert-color -replace-color.title=Replace-Invert-Color -replace-color.header=Zameni-inverziranje boja u PDF-u -home.replaceColorPdf.title=Replace and Invert Color -home.replaceColorPdf.desc=Zamenite boju teksta i pozadine u PDF-u te inverzirajte cijeli PDF kako bi se smanjila veličina datoteke. -replaceColorPdf.tags=Zameni boju, Rad sa stranicama, Back end, server strana -replace-color.selectText.1=Optije za zamenu ili inverziranje boja -replace-color.selectText.2=Standardno (standarske visoko kontrastne boje) -replace-color.selectText.3=Napčno (prilagođene boje) -replace-color.selectText.4=Cijelo-inverzirajte (inverzirajte sve boje) -replace-color.selectText.5=Optije visoko kontrastne boje -replace-color.selectText.6=Crna tekst na bijelu pozadini -replace-color.selectText.7=Bijeli tekst na crvenoj pozadini -replace-color.selectText.8=Žutni tekst na crnoj pozadini +replace-color.title=Zamijeni/invertiraj boju +replace-color.header=Zamijeni/invertiraj boju PDF-a +home.replaceColorPdf.title=Zamijeni i invertiraj boju +home.replaceColorPdf.desc=Zamijenite boju teksta i pozadine u PDF-u te invertirajte cijelu boju PDF-a kako biste smanjili veličinu datoteke +replaceColorPdf.tags=zamijeni boju,operacije sa stranicama,poslužiteljska strana +replace-color.selectText.1=Opcije za zamjenu ili invertiranje boja +replace-color.selectText.2=Zadano (zadane boje visokog kontrasta) +replace-color.selectText.3=Prilagođeno (prilagođene boje) +replace-color.selectText.4=Potpuno invertiranje (invertiraj sve boje) +replace-color.selectText.5=Opcije boja visokog kontrasta +replace-color.selectText.6=Bijeli tekst na crnoj pozadini +replace-color.selectText.7=Crni tekst na bijeloj pozadini +replace-color.selectText.8=Žuti tekst na crnoj pozadini replace-color.selectText.9=Zeleni tekst na crnoj pozadini -replace-color.selectText.10=Izaberite boju teksta -replace-color.selectText.11=Izaberite pozadinu boju +replace-color.selectText.10=Odaberite boju teksta +replace-color.selectText.11=Odaberite boju pozadine +replace-color.selectText.12=Colour Space Conversion (CMYK for Printing) +replace-color.selectText.13=CMYK Colour Space Conversion +replace-color.selectText.14=This option converts the PDF from RGB colour space to CMYK colour space, which is optimized for professional printing. This process: +replace-color.selectText.15=Converts colours to CMYK (Cyan, Magenta, Yellow, Black) colour model used by professional printers +replace-color.selectText.16=Optimizes the PDF for print production with prepress settings +replace-color.selectText.17=May result in slight colour changes as CMYK has a smaller colour gamut than RGB replace-color.submit=Zamijeni @@ -886,130 +910,130 @@ replace-color.submit=Zamijeni # # ########################### #login -login.title=Prijavite se -login.header=Prijavite se -login.signin=Prijavite se +login.title=Prijava +login.header=Prijava +login.signin=Prijavi se login.rememberme=Zapamti me -login.invalid=Neispravno korisničko ime ili zaporka. +login.invalid=Neispravno korisničko ime ili lozinka. login.locked=Vaš račun je zaključan. -login.signinTitle=Molimo vas da se prijavite -login.ssoSignIn=Prijavite se putem jedinstvene prijave -login.oAuth2AutoCreateDisabled=OAUTH2 automatsko kreiranje korisnika je onemogućeno -login.oAuth2AdminBlockedUser=Registracija ili prijava nekadreguiranih korisnika trenutno su blokirane. Molimo Vas da kontaktirate administratora. +login.signinTitle=Molimo prijavite se +login.ssoSignIn=Prijava putem jedinstvene prijave (SSO) +login.oAuth2AutoCreateDisabled=Automatsko kreiranje korisnika putem OAUTH2 je onemogućeno +login.oAuth2AdminBlockedUser=Registracija ili prijava neregistriranih korisnika trenutno je blokirana. Molimo kontaktirajte administratora. login.oauth2RequestNotFound=Zahtjev za autorizaciju nije pronađen -login.oauth2InvalidUserInfoResponse=Nevažeće informacije o korisniku +login.oauth2InvalidUserInfoResponse=Nevažeći odgovor s informacijama o korisniku login.oauth2invalidRequest=Neispravan zahtjev login.oauth2AccessDenied=Pristup odbijen login.oauth2InvalidTokenResponse=Nevažeći odgovor tokena login.oauth2InvalidIdToken=Nevažeći ID token -login.relyingPartyRegistrationNotFound=No relying party registration found -login.userIsDisabled=Korisnik je deaktiviran, prijava sa ovim korisničkim imenom je trenutno zakazana. Molimo Vas da kontaktirate administratorske osobe. -login.alreadyLoggedIn=Već ste se prijavili na -login.alreadyLoggedIn2=ure. Odjavite se s ure i pokušajte ponovo. -login.toManySessions=Imate preko mrežne sesije aktivnih -login.logoutMessage=You have been logged out. -login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator. +login.relyingPartyRegistrationNotFound=Nije pronađena registracija pouzdane strane +login.userIsDisabled=Korisnik je deaktiviran, prijava s ovim korisničkim imenom je trenutno blokirana. Molimo kontaktirajte administratora. +login.alreadyLoggedIn=Već ste prijavljeni na +login.alreadyLoggedIn2=uređaja. Molimo odjavite se s uređaja i pokušajte ponovo. +login.toManySessions=Imate previše aktivnih sesija +login.logoutMessage=Odjavljeni ste. +login.invalidInResponseTo=Traženi SAML odgovor je nevažeći ili je istekao. Molimo kontaktirajte administratora. #auto-redact -autoRedact.title=Automatsko uređivanje -autoRedact.header=Automatsko uređivanje +autoRedact.title=Automatsko zatamnjivanje +autoRedact.header=Automatsko zatamnjivanje autoRedact.colorLabel=Boja -autoRedact.textsToRedactLabel=Tekst za uređivanje (razdvojen linijama) -autoRedact.textsToRedactPlaceholder=npr. \nPovjerljivo \nStrogo čuvana tajna -autoRedact.useRegexLabel=Koristi Regex +autoRedact.textsToRedactLabel=Tekst za zatamnjivanje (odvojen redcima) +autoRedact.textsToRedactPlaceholder=npr. \nPovjerljivo\nStrogo povjerljivo +autoRedact.useRegexLabel=Koristi regularne izraze autoRedact.wholeWordSearchLabel=Pretraživanje cijelih riječi -autoRedact.customPaddingLabel=Dodatni prazan prostor -autoRedact.convertPDFToImageLabel=Pretvorite PDF u PDF-sliku (koristi se za uklanjanje teksta iza okvira) -autoRedact.submitButton=Potvrdi +autoRedact.customPaddingLabel=Prilagođeni dodatni razmak +autoRedact.convertPDFToImageLabel=Pretvori PDF u PDF-sliku (koristi se za uklanjanje teksta iza okvira) +autoRedact.submitButton=Pošalji #redact -redact.title=Manual Redaction -redact.header=Manual Redaction -redact.submit=Redact -redact.textBasedRedaction=Text based Redaction -redact.pageBasedRedaction=Page-based Redaction -redact.convertPDFToImageLabel=Convert PDF to PDF-Image (Used to remove text behind the box) -redact.pageRedactionNumbers.title=Pages -redact.pageRedactionNumbers.placeholder=(e.g. 1,2,8 or 4,7,12-16 or 2n-1) -redact.redactionColor.title=Redaction Color -redact.export=Export -redact.upload=Upload -redact.boxRedaction=Box draw redaction -redact.zoom=Zoom -redact.zoomIn=Zoom in -redact.zoomOut=Zoom out -redact.nextPage=Next Page -redact.previousPage=Previous Page -redact.toggleSidebar=Toggle Sidebar -redact.showThumbnails=Show Thumbnails -redact.showDocumentOutline=Show Document Outline (double-click to expand/collapse all items) -redact.showAttatchments=Show Attachments -redact.showLayers=Show Layers (double-click to reset all layers to the default state) -redact.colourPicker=Colour Picker -redact.findCurrentOutlineItem=Find current outline item -redact.applyChanges=Apply Changes +redact.title=Ručno zatamnjivanje +redact.header=Ručno zatamnjivanje +redact.submit=Zatamni +redact.textBasedRedaction=Zatamnjivanje temeljeno na tekstu +redact.pageBasedRedaction=Zatamnjivanje temeljeno na stranici +redact.convertPDFToImageLabel=Pretvori PDF u PDF-sliku (koristi se za uklanjanje teksta iza okvira) +redact.pageRedactionNumbers.title=Stranice +redact.pageRedactionNumbers.placeholder=(npr. 1,2,8 ili 4,7,12-16 ili 2n-1) +redact.redactionColor.title=Boja zatamnjivanja +redact.export=Izvezi +redact.upload=Učitaj +redact.boxRedaction=Zatamnjivanje crtanjem okvira +redact.zoom=Zumiraj +redact.zoomIn=Povećaj +redact.zoomOut=Smanji +redact.nextPage=Sljedeća stranica +redact.previousPage=Prethodna stranica +redact.toggleSidebar=Prebaci bočnu traku +redact.showThumbnails=Prikaži sličice +redact.showDocumentOutline=Prikaži sadržaj dokumenta (dvostruki klik za proširenje/sažimanje svih stavki) +redact.showAttatchments=Prikaži privitke +redact.showLayers=Prikaži slojeve (dvostruki klik za resetiranje svih slojeva na zadano stanje) +redact.colourPicker=Birač boja +redact.findCurrentOutlineItem=Pronađi trenutnu stavku u sadržaju +redact.applyChanges=Primijeni promjene #showJS showJS.title=Prikaži Javascript showJS.header=Prikaži Javascript -showJS.downloadJS=Preuzmite Javascript +showJS.downloadJS=Preuzmi Javascript showJS.submit=Prikaži #pdfToSinglePage -pdfToSinglePage.title=PDF u Jednu Stranicu -pdfToSinglePage.header=PDF u Jednu Stranicu -pdfToSinglePage.submit=Pretvori u Jednu Stranicu +pdfToSinglePage.title=PDF u jednu stranicu +pdfToSinglePage.header=PDF u jednu stranicu +pdfToSinglePage.submit=Pretvori u jednu stranicu #pageExtracter -pageExtracter.title=Izdvojiti stranice -pageExtracter.header=Izdvojiti stranice +pageExtracter.title=Izdvoji stranice +pageExtracter.header=Izdvoji stranice pageExtracter.submit=Izdvoji -pageExtracter.placeholder=(t.j. 1,2,8 ili 4,7,12-16 ili 2n-1) +pageExtracter.placeholder=(npr. 1,2,8 ili 4,7,12-16 ili 2n-1) #getPdfInfo -getPdfInfo.title=Informacije o PDF-u -getPdfInfo.header=Informacije o PDF-u -getPdfInfo.submit=Informacije -getPdfInfo.downloadJson=Preuzmite JSON -getPdfInfo.summary=PDF Summary -getPdfInfo.summary.encrypted=This PDF is encrypted so may face issues with some applications -getPdfInfo.summary.permissions=This PDF has {0} restricted permissions which may limit what you can do with it -getPdfInfo.summary.compliance=This PDF complies with the {0} standard -getPdfInfo.summary.basicInfo=Basic Information -getPdfInfo.summary.docInfo=Document Information -getPdfInfo.summary.encrypted.alert=Encrypted PDF - This document is password protected -getPdfInfo.summary.not.encrypted.alert=Unencrypted PDF - No password protection -getPdfInfo.summary.permissions.alert=Restricted Permissions - {0} actions are not allowed -getPdfInfo.summary.all.permissions.alert=All Permissions Allowed -getPdfInfo.summary.compliance.alert={0} Compliant -getPdfInfo.summary.no.compliance.alert=No Compliance Standards -getPdfInfo.summary.security.section=Security Status -getPdfInfo.section.BasicInfo=Basic Information about the PDF document including file size, page count, and language -getPdfInfo.section.Metadata=Document metadata including title, author, creation date and other document properties -getPdfInfo.section.DocumentInfo=Technical details about the PDF document structure and version -getPdfInfo.section.Compliancy=PDF standards compliance information (PDF/A, PDF/X, etc.) -getPdfInfo.section.Encryption=Security and encryption details of the document -getPdfInfo.section.Permissions=Document permission settings that control what actions can be performed -getPdfInfo.section.Other=Additional document components like bookmarks, layers, and embedded files -getPdfInfo.section.FormFields=Interactive form fields present in the document -getPdfInfo.section.PerPageInfo=Detailed information about each page in the document +getPdfInfo.title=Dohvati informacije o PDF-u +getPdfInfo.header=Dohvati informacije o PDF-u +getPdfInfo.submit=Dohvati informacije +getPdfInfo.downloadJson=Preuzmi JSON +getPdfInfo.summary=Sažetak PDF-a +getPdfInfo.summary.encrypted=Ovaj PDF je šifriran pa se mogu pojaviti problemi s nekim aplikacijama +getPdfInfo.summary.permissions=Ovaj PDF ima {0} ograničenih dopuštenja što može ograničiti što možete učiniti s njim +getPdfInfo.summary.compliance=Ovaj PDF je u skladu sa standardom {0} +getPdfInfo.summary.basicInfo=Osnovne informacije +getPdfInfo.summary.docInfo=Informacije o dokumentu +getPdfInfo.summary.encrypted.alert=Šifrirani PDF - Ovaj dokument je zaštićen lozinkom +getPdfInfo.summary.not.encrypted.alert=Nešifrirani PDF - Nema zaštite lozinkom +getPdfInfo.summary.permissions.alert=Ograničena dopuštenja - {0} radnji nije dopušteno +getPdfInfo.summary.all.permissions.alert=Sva dopuštenja su dopuštena +getPdfInfo.summary.compliance.alert={0} sukladan +getPdfInfo.summary.no.compliance.alert=Nema standarda sukladnosti +getPdfInfo.summary.security.section=Sigurnosni status +getPdfInfo.section.BasicInfo=Osnovne informacije o PDF dokumentu uključujući veličinu datoteke, broj stranica i jezik +getPdfInfo.section.Metadata=Metapodaci dokumenta uključujući naslov, autora, datum stvaranja i druga svojstva dokumenta +getPdfInfo.section.DocumentInfo=Tehnički detalji o strukturi i verziji PDF dokumenta +getPdfInfo.section.Compliancy=Informacije o usklađenosti s PDF standardima (PDF/A, PDF/X, itd.) +getPdfInfo.section.Encryption=Detalji o sigurnosti i šifriranju dokumenta +getPdfInfo.section.Permissions=Postavke dopuštenja dokumenta koje kontroliraju koje se radnje mogu izvršiti +getPdfInfo.section.Other=Dodatne komponente dokumenta poput oznaka, slojeva i ugrađenih datoteka +getPdfInfo.section.FormFields=Interaktivna polja obrasca prisutna u dokumentu +getPdfInfo.section.PerPageInfo=Detaljne informacije o svakoj stranici u dokumentu #markdown-to-pdf MarkdownToPDF.title=Markdown u PDF MarkdownToPDF.header=Markdown u PDF MarkdownToPDF.submit=Pretvori -MarkdownToPDF.help=Rad u toku +MarkdownToPDF.help=U izradi MarkdownToPDF.credit=Koristi WeasyPrint #pdf-to-markdown -PDFToMarkdown.title=PDF To Markdown -PDFToMarkdown.header=PDF To Markdown -PDFToMarkdown.submit=Convert +PDFToMarkdown.title=PDF u Markdown +PDFToMarkdown.header=PDF u Markdown +PDFToMarkdown.submit=Pretvori #url-to-pdf @@ -1022,18 +1046,18 @@ URLToPDF.credit=Koristi WeasyPrint #html-to-pdf HTMLToPDF.title=HTML u PDF HTMLToPDF.header=HTML u PDF -HTMLToPDF.help=Prihvaća HTML datoteke i ZIP-ove koji sadrže html/css/slike itd. potrebno +HTMLToPDF.help=Prihvaća HTML datoteke i ZIP arhive koje sadrže potrebne html/css/slike itd. HTMLToPDF.submit=Pretvori HTMLToPDF.credit=Koristi WeasyPrint HTMLToPDF.zoom=Razina zumiranja za prikaz web stranice. -HTMLToPDF.pageWidth=Širina stranice u centimetrima. (Prazno u Zadano) -HTMLToPDF.pageHeight=Visina stranice u centimetrima. (Prazno u Zadano) -HTMLToPDF.marginTop=Gornja margina stranice u milimetrima. (Prazno u Zadano) -HTMLToPDF.marginBottom=Donja margina stranice u milimetrima. (Prazno u Zadano) -HTMLToPDF.marginLeft=Lijeva margina stranice u milimetrima. (Prazno u Zadano) -HTMLToPDF.marginRight=Desna margina stranice u milimetrima. (Prazno u Zadano) -HTMLToPDF.printBackground=Prikaz pozadine web stranica. -HTMLToPDF.defaultHeader=Omogući zadano zaglavlje (Ime i broj stranice) +HTMLToPDF.pageWidth=Širina stranice u centimetrima (prazno za zadano). +HTMLToPDF.pageHeight=Visina stranice u centimetrima (prazno za zadano). +HTMLToPDF.marginTop=Gornja margina stranice u milimetrima (prazno za zadano). +HTMLToPDF.marginBottom=Donja margina stranice u milimetrima (prazno za zadano). +HTMLToPDF.marginLeft=Lijeva margina stranice u milimetrima (prazno za zadano). +HTMLToPDF.marginRight=Desna margina stranice u milimetrima (prazno za zadano). +HTMLToPDF.printBackground=Renderiraj pozadinu web stranica. +HTMLToPDF.defaultHeader=Omogući zadano zaglavlje (ime i broj stranice). HTMLToPDF.cssMediaType=Promijenite vrstu CSS medija stranice. HTMLToPDF.none=Nijedan HTMLToPDF.print=Ispis @@ -1041,69 +1065,69 @@ HTMLToPDF.screen=Zaslon #AddStampRequest -AddStampRequest.header=Pečat PDF -AddStampRequest.title=Pečat PDF -AddStampRequest.stampType=Pečat Tip -AddStampRequest.stampText=Pečat Tekst -AddStampRequest.stampImage=Pečat Slika +AddStampRequest.header=Dodaj pečat u PDF +AddStampRequest.title=Dodaj pečat u PDF +AddStampRequest.stampType=Vrsta pečata +AddStampRequest.stampText=Tekst pečata +AddStampRequest.stampImage=Slika pečata AddStampRequest.alphabet=Abeceda AddStampRequest.fontSize=Veličina fonta/slike AddStampRequest.rotation=Rotacija AddStampRequest.opacity=Neprozirnost AddStampRequest.position=Položaj -AddStampRequest.overrideX=Poništi X koordinatu -AddStampRequest.overrideY=Poništi Y koordinatu +AddStampRequest.overrideX=Prebriši X koordinatu +AddStampRequest.overrideY=Prebriši Y koordinatu AddStampRequest.customMargin=Prilagođena margina AddStampRequest.customColor=Prilagođena boja teksta AddStampRequest.submit=Pošalji #sanitizePDF -sanitizePDF.title=Sanirajte PDF -sanitizePDF.header=Sanirajte PDF datoteku +sanitizePDF.title=Saniraj PDF +sanitizePDF.header=Saniraj PDF datoteku sanitizePDF.selectText.1=Ukloni JavaScript akcije sanitizePDF.selectText.2=Ukloni ugrađene datoteke -sanitizePDF.selectText.3=Remove XMP metadata +sanitizePDF.selectText.3=Ukloni XMP metapodatke sanitizePDF.selectText.4=Ukloni poveznice -sanitizePDF.selectText.5=Uklonite fontove -sanitizePDF.selectText.6=Remove Document Info Metadata -sanitizePDF.submit=Sanirajte PDF +sanitizePDF.selectText.5=Ukloni fontove +sanitizePDF.selectText.6=Ukloni metapodatke o dokumentu +sanitizePDF.submit=Saniraj PDF #addPageNumbers -addPageNumbers.title=Dodavanje brojeva stranica -addPageNumbers.header=Dodavanje brojeva stranica +addPageNumbers.title=Dodaj brojeve stranica +addPageNumbers.header=Dodaj brojeve stranica addPageNumbers.selectText.1=Odaberi PDF datoteku: addPageNumbers.selectText.2=Veličina margine addPageNumbers.selectText.3=Položaj addPageNumbers.selectText.4=Početni broj -addPageNumbers.selectText.5=Brojanje stranica +addPageNumbers.selectText.5=Stranice za numeriranje addPageNumbers.selectText.6=Prilagođeni tekst addPageNumbers.customTextDesc=Prilagođeni tekst addPageNumbers.numberPagesDesc=Koje stranice numerirati, zadano je 'sve', također prihvaća 1-5 ili 2,5,9 itd. -addPageNumbers.customNumberDesc=Zadano je {n}, također prihvaća 'Stranica {n} od {total}', 'Tekst-{n}', '{ime datoteke}-{n}' +addPageNumbers.customNumberDesc=Zadano je {n}, također prihvaća 'Stranica {n} od {total}', 'Tekst-{n}', '{filename}-{n}' addPageNumbers.submit=Dodaj brojeve stranica #auto-rename -auto-rename.title=Automatski preimenuj -auto-rename.header=Automatski preimenuj PDF +auto-rename.title=Automatsko preimenovanje +auto-rename.header=Automatsko preimenovanje PDF-a auto-rename.submit=Automatski preimenuj #adjustContrast -adjustContrast.title=Podesite kontrast -adjustContrast.header=Podesite kontrast +adjustContrast.title=Podesi kontrast +adjustContrast.header=Podesi kontrast adjustContrast.contrast=Kontrast: -adjustContrast.brightness=Osvjetljenje: -adjustContrast.saturation=Zasićenje: +adjustContrast.brightness=Svjetlina: +adjustContrast.saturation=Zasićenost: adjustContrast.download=Preuzmi #crop crop.title=Izreži -crop.header=Izreži sliku -crop.submit=Potvrdi +crop.header=Izreži PDF +crop.submit=Pošalji #autoSplitPDF @@ -1111,124 +1135,125 @@ autoSplitPDF.title=Automatsko dijeljenje PDF-a autoSplitPDF.header=Automatsko dijeljenje PDF-a autoSplitPDF.description=Ispišite, umetnite, skenirajte, učitajte i dopustite nam da automatski odvojimo vaše dokumente. Nije potrebno ručno sortiranje. autoSplitPDF.selectText.1=Ispišite nekoliko razdjelnih listova odozdo (crno-bijelo je u redu). -autoSplitPDF.selectText.2=Skenirajte sve dokumente odjednom umetanjem razdjelnog lista između njih. -autoSplitPDF.selectText.3=Prenesite jednu veliku skeniranu PDF datoteku i pustite našem PDF-u da se pobrine za ostalo. +autoSplitPDF.selectText.2=Skenirajte sve svoje dokumente odjednom umetanjem razdjelnog lista između njih. +autoSplitPDF.selectText.3=Učitajte jednu veliku skeniranu PDF datoteku i pustite Stirling PDF da se pobrine za ostalo. autoSplitPDF.selectText.4=Razdjelne stranice automatski se otkrivaju i uklanjaju, jamčeći uredan konačni dokument. -autoSplitPDF.formPrompt=Pošaljite PDF koji sadrži naše razdjelnike stranica: +autoSplitPDF.formPrompt=Pošaljite PDF koji sadrži razdjelnike stranica Stirling-PDF-a: autoSplitPDF.duplexMode=Obostrani način rada (skeniranje s prednje i stražnje strane) -autoSplitPDF.dividerDownload2=Preuzmite 'Auto Splitter Divider (s uputama).pdf' -autoSplitPDF.submit=Potvrdi +autoSplitPDF.dividerDownload2=Preuzmite 'Automatski razdjelnik (s uputama).pdf' +autoSplitPDF.submit=Pošalji #pipeline -pipeline.title=Tok rada +pipeline.title=Automatizacija #pageLayout -pageLayout.title=Izgled s više stranica -pageLayout.header=Izgled s više stranica -pageLayout.pagesPerSheet=Broj stranica po listu: -pageLayout.addBorder=Dodajte granice dokumenta -pageLayout.submit=Potvrdi +pageLayout.title=Raspored s više stranica +pageLayout.header=Raspored s više stranica +pageLayout.pagesPerSheet=Stranica po listu: +pageLayout.addBorder=Dodaj obrube +pageLayout.submit=Pošalji #scalePages -scalePages.title=Podesite veličinu stranice -scalePages.header=Podesite veličinu stranice +scalePages.title=Podesi mjerilo stranice +scalePages.header=Podesi mjerilo stranice scalePages.pageSize=Veličina stranice dokumenta. scalePages.keepPageSize=Originalna veličina scalePages.scaleFactor=Razina zumiranja (obrezivanje) stranice. -scalePages.submit=Potvrdi +scalePages.submit=Pošalji #certSign -certSign.title=Potpisivanje Certifikatom -certSign.header=Potpišite PDF svojim certifikatom (Rad u tijeku) +certSign.title=Potpisivanje certifikatom +certSign.header=Potpišite PDF svojim certifikatom (u izradi) certSign.selectPDF=Odaberite PDF datoteku za potpisivanje: certSign.jksNote=Napomena: Ako vrsta vašeg certifikata nije navedena u nastavku, pretvorite ga u datoteku Java Keystore (.jks) pomoću alata naredbenog retka keytool. Zatim odaberite opciju .jks datoteke u nastavku. certSign.selectKey=Odaberite svoju datoteku privatnog ključa (format PKCS#8, može biti .pem ili .der): -certSign.selectCert=Odaberite svoju datoteku certifikata (format X.509, može biti .pem ili .der): +certSign.selectCert=Odaberite svoju datoteku certifikata (X.509 format, može biti .pem ili .der): certSign.selectP12=Odaberite svoju PKCS#12 datoteku pohrane ključeva (.p12 ili .pfx) (neobavezno, ako je dostupna, trebala bi sadržavati vaš privatni ključ i certifikat): -certSign.selectJKS=Odaberite datoteku Java Keystore (.jks ili .keystore): +certSign.selectJKS=Odaberite svoju Java Keystore datoteku (.jks ili .keystore): certSign.certType=Tip certifikata -certSign.password=Unesite svoju lozinku za skladište ključeva ili privatni ključ (ako postoji): +certSign.password=Unesite svoju lozinku za pohranu ključeva ili privatni ključ (ako postoji): certSign.showSig=Prikaži potpis certSign.reason=Razlog -certSign.location=Mjesto +certSign.location=Lokacija certSign.name=Ime -certSign.showLogo=Prikaži logo +certSign.showLogo=Prikaži logotip certSign.submit=Potpiši PDF #removeCertSign -removeCertSign.title=Ukloni digitalno potpisano dokazilo -removeCertSign.header=Uklonite digitalni potpis iz PDF-a -removeCertSign.selectPDF=Odaberite datoteku PDF: -removeCertSign.submit=Ukloni potpisi +removeCertSign.title=Ukloni potpis certifikata +removeCertSign.header=Uklonite digitalni certifikat iz PDF-a +removeCertSign.selectPDF=Odaberite PDF datoteku: +removeCertSign.submit=Ukloni potpis #removeBlanks -removeBlanks.title=Uklonite prazne stranice -removeBlanks.header=Uklonite prazne stranice +removeBlanks.title=Ukloni prazne stranice +removeBlanks.header=Ukloni prazne stranice removeBlanks.threshold=Prag bjeline piksela: removeBlanks.thresholdDesc=Prag za određivanje koliko bijeli piksel mora biti bijel da bi bio klasificiran kao 'bijeli'. 0 = crno, 255 čisto bijelo. removeBlanks.whitePercent=Postotak bijele boje (%): -removeBlanks.whitePercentDesc=Postotak stranice koji mora biti "bijeli" piksel da bi se uklonio -removeBlanks.submit=Uklonite prazne stranice +removeBlanks.whitePercentDesc=Postotak stranice koji mora biti 'bijeli' piksel da bi se uklonio +removeBlanks.submit=Ukloni prazne stranice #removeAnnotations -removeAnnotations.title=Ukloni komentare -removeAnnotations.header=Ukloni komentare +removeAnnotations.title=Ukloni bilješke +removeAnnotations.header=Ukloni bilješke removeAnnotations.submit=Ukloni #compare -compare.title=Uporedite -compare.header=Usporedite PDF-ove -compare.highlightColor.1=Boja osvetljenja 1: -compare.highlightColor.2=Boja osvetljenja 2: +compare.title=Usporedi +compare.header=Usporedi PDF-ove +compare.highlightColor.1=Boja isticanja 1: +compare.highlightColor.2=Boja isticanja 2: compare.document.1=Dokument 1 compare.document.2=Dokument 2 -compare.submit=Uporedi -compare.complex.message=Jedan ili oba unesena dokumenta su veliki datoteke, to može smanjiti preciznost usporedbi -compare.large.file.message=Jedan ili oba unesena dokumenta su prevelike za obradu -compare.no.text.message=Jedan ili oba odabrana PDF-a nema tekst. Odaberite PDF-ove s tekstom za usporedbu. +compare.submit=Usporedi +compare.complex.message=Jedan ili oba dostavljena dokumenta su velike datoteke, točnost usporedbe može biti smanjena +compare.large.file.message=Jedan ili oba dostavljena dokumenta su preveliki za obradu +compare.no.text.message=Jedan ili oba odabrana PDF-a nemaju tekstualni sadržaj. Molimo odaberite PDF-ove s tekstom za usporedbu. #sign -sign.title=Potpišite -sign.header=Potpišite PDF-ove +sign.title=Potpiši +sign.header=Potpiši PDF-ove sign.upload=Učitaj sliku sign.draw=Nacrtaj potpis sign.text=Tekstualni unos sign.clear=Obriši sign.add=Dodaj -sign.saved=Sacuvane potpisne oznake -sign.save=Sačuvaj potpisnu oznaku +sign.saved=Spremljeni potpisi +sign.save=Spremi potpis sign.personalSigs=Osobni potpisi sign.sharedSigs=Dijeljeni potpisi -sign.noSavedSigs=Nema sacuvanih potpisa pronađenih -sign.addToAll=Add to all pages -sign.delete=Delete -sign.first=First page -sign.last=Last page -sign.next=Next page -sign.previous=Previous page -sign.maintainRatio=Toggle maintain aspect ratio -sign.undo=Undo -sign.redo=Redo +sign.noSavedSigs=Nema spremljenih potpisa +sign.addToAll=Dodaj na sve stranice +sign.delete=Izbriši +sign.first=Prva stranica +sign.last=Zadnja stranica +sign.next=Sljedeća stranica +sign.previous=Prethodna stranica +sign.maintainRatio=Prebaci zadržavanje omjera slike +sign.undo=Poništi +sign.redo=Ponovi +sign.colour=Signature Colour #repair repair.title=Popravi -repair.header=Popravi PDF datoteku +repair.header=Popravi PDF-ove repair.submit=Popravi #flatten -flatten.title=Izravnati -flatten.header=Izravnati pdf -flatten.flattenOnlyForms=Izravnati samo obrasce -flatten.submit=Izravnati +flatten.title=Poravnaj +flatten.header=Poravnaj PDF-ove +flatten.flattenOnlyForms=Poravnaj samo obrasce +flatten.submit=Poravnaj #ScannerImageSplit @@ -1238,61 +1263,61 @@ ScannerImageSplit.selectText.3=Tolerancija: ScannerImageSplit.selectText.4=Određuje raspon varijacije boje oko procijenjene boje pozadine (zadano: 30). ScannerImageSplit.selectText.5=Minimalna površina: ScannerImageSplit.selectText.6=Postavlja minimalni prag površine za fotografiju (zadano: 10000). -ScannerImageSplit.selectText.7=Minimalna konturna površina: +ScannerImageSplit.selectText.7=Minimalna površina konture: ScannerImageSplit.selectText.8=Postavlja minimalni prag površine konture za fotografiju ScannerImageSplit.selectText.9=Veličina obruba: -ScannerImageSplit.selectText.10=Postavlja veličinu obruba koji se dodaje i uklanja kako bi se spriječili bijeli obrubi u ispisu (zadano: 1). -ScannerImageSplit.info=Python nije instaliran. Treba je za izvršenje. +ScannerImageSplit.selectText.10=Postavlja veličinu obruba koji se dodaje i uklanja kako bi se spriječili bijeli obrubi u izlazu (zadano: 1). +ScannerImageSplit.info=Python nije instaliran. Potreban je za pokretanje. #OCR -ocr.title=OCR / čišćenje skeniranja -ocr.header=Čišćenje skeniranja / OCR (optičko prepoznavanje znakova) +ocr.title=OCR / Čišćenje skenova +ocr.header=Čišćenje skenova / OCR (Optičko prepoznavanje znakova) ocr.selectText.1=Odaberite jezike koji će se otkriti unutar PDF-a (navedeni su oni koji su trenutno otkriveni): -ocr.selectText.2=Izradite tekstualnu datoteku koja sadrži OCR tekst uz OCR-ovani PDF -ocr.selectText.3=Ispravne stranice su skenirane pod nagnutim kutom rotiranjem na mjesto -ocr.selectText.4=Očistite stranicu tako da je manja vjerojatnost da će OCR pronaći tekst u pozadinskoj buci. (Bez promjene izlaza) -ocr.selectText.5=Očisti stranicu tako da je manja vjerojatnost da će OCR pronaći tekst u pozadinskoj buci, održava čišćenje u izlazu. -ocr.selectText.6=Ignorira stranice koje na sebi imaju interaktivni tekst, samo OCR stranice koje su slike -ocr.selectText.7=Prinudni OCR, OCR će za svaku stranicu ukloniti sve izvorne elemente teksta -ocr.selectText.8=Normalno (Bit će pogreška ako PDF sadrži tekst) +ocr.selectText.2=Izradite tekstualnu datoteku koja sadrži OCR tekst uz OCR-irani PDF +ocr.selectText.3=Ispravite stranice koje su skenirane pod kosim kutom rotiranjem natrag na mjesto +ocr.selectText.4=Očistite stranicu tako da je manja vjerojatnost da će OCR pronaći tekst u pozadinskoj buci (bez promjene izlaza). +ocr.selectText.5=Očistite stranicu tako da je manja vjerojatnost da će OCR pronaći tekst u pozadinskoj buci, održava čišćenje u izlazu. +ocr.selectText.6=Ignorira stranice koje na sebi imaju interaktivni tekst, OCR-ira samo stranice koje su slike +ocr.selectText.7=Prisilni OCR, OCR-irat će svaku stranicu uklanjajući sve izvorne elemente teksta +ocr.selectText.8=Normalno (Javit će grešku ako PDF sadrži tekst) ocr.selectText.9=Dodatne postavke ocr.selectText.10=OCR način -ocr.selectText.11=Ukloni slike nakon OCR-a (Uklanja SVE slike, korisno samo ako je dio koraka konverzije) -ocr.selectText.12=Vrsta iscrtavanja (napredno) -ocr.help=Pročitajte ovu dokumentaciju o tome kako ovo koristiti za druge jezike i/ili koristiti ne u dockeru +ocr.selectText.11=Ukloni slike nakon OCR-a (Uklanja SVE slike, korisno samo ako je dio koraka pretvorbe) +ocr.selectText.12=Vrsta renderiranja (napredno) +ocr.help=Molimo pročitajte ovu dokumentaciju o tome kako ovo koristiti za druge jezike i/ili koristiti izvan Dockera ocr.credit=Ova usluga koristi qpdf i Tesseract za OCR. -ocr.submit=Obradi PDF sa OCR-om +ocr.submit=Obradi PDF s OCR-om #extractImages -extractImages.title=Ekstrakt slika -extractImages.header=Ekstrakt slika +extractImages.title=Izdvoji slike +extractImages.header=Izdvoji slike extractImages.selectText=Odaberite format slike za pretvaranje izdvojenih slika -extractImages.allowDuplicates=Sačuvaj duplikate slike -extractImages.submit=Izdvajanje +extractImages.allowDuplicates=Spremi duplikate slika +extractImages.submit=Izdvoji #File to PDF -fileToPDF.title=datoteku u PDF -fileToPDF.header=Pretvori bilo koji datoteku u PDF +fileToPDF.title=Datoteka u PDF +fileToPDF.header=Pretvori bilo koju datoteku u PDF fileToPDF.credit=Ova usluga koristi LibreOffice i Unoconv za pretvaranje datoteka. fileToPDF.supportedFileTypesInfo=Podržane vrste datoteka -fileToPDF.supportedFileTypes=Podržane vrste datoteka trebale bi uključivati dolje, no za potpuni ažurirani popis podržanih formata pogledajte dokumentaciju LibreOfficea +fileToPDF.supportedFileTypes=Podržane vrste datoteka trebale bi uključivati dolje navedene, no za potpuni ažurirani popis podržanih formata, molimo pogledajte dokumentaciju LibreOfficea fileToPDF.submit=Pretvori u PDF #compress -compress.title=Komprimirajte -compress.header=Komprimirajte PDF -compress.credit=Ova usluga koristi qpdf za komprimiranje / optimizaciju PDF-a. +compress.title=Komprimiraj +compress.header=Komprimiraj PDF +compress.credit=Ova usluga koristi qpdf za komprimiranje/optimizaciju PDF-a. compress.grayscale.label=Primijeni sivinu za kompresiju -compress.selectText.1=Compression Settings -compress.selectText.1.1=1-3 PDF compression,
4-6 lite image compression,
7-9 intense image compression Will dramatically reduce image quality -compress.selectText.2=Nivo optimizacije: +compress.selectText.1=Postavke kompresije +compress.selectText.1.1=1-3 kompresija PDF-a,
4-6 lagana kompresija slike,
7-9 intenzivna kompresija slike (drastično će smanjiti kvalitetu slike) +compress.selectText.2=Razina optimizacije: compress.selectText.4=Automatski način - Automatski prilagođava kvalitetu kako bi PDF dobio točnu veličinu -compress.selectText.5=Očekivana veličina PDF-a (npr. 25 MB, 10,8 MB, 25 KB) -compress.submit=Kompresiraj +compress.selectText.5=Očekivana veličina PDF-a (npr. 25MB, 10.8MB, 25KB) +compress.submit=Komprimiraj #Add image @@ -1303,111 +1328,111 @@ addImage.upload=Dodaj sliku addImage.submit=Dodaj sliku #attachments -attachments.title=Add Attachments -attachments.header=Add attachments -attachments.description=Allows you to add attachments to the PDF -attachments.descriptionPlaceholder=Enter a description for the attachments... -attachments.addButton=Add Attachments +attachments.title=Dodaj privitke +attachments.header=Dodaj privitke +attachments.description=Omogućuje dodavanje privitaka u PDF +attachments.descriptionPlaceholder=Unesite opis za privitke... +attachments.addButton=Dodaj privitke #merge merge.title=Spajanje merge.header=Spajanje više PDF-ova (2+) merge.sortByName=Poredaj po imenu merge.sortByDate=Poredaj po datumu -merge.removeCertSign=Ukloniti digitalni potpis u kombiniranom datoteku? -merge.generateToc=Generate table of contents in the merged file? -merge.submit=Spajanje +merge.removeCertSign=Ukloniti digitalni potpis u spojenoj datoteci? +merge.generateToc=Generirati sadržaj u spojenoj datoteci? +merge.submit=Spoji #pdfOrganiser pdfOrganiser.title=Organizator stranica -pdfOrganiser.header=Organizator stranica u PDF-u -pdfOrganiser.submit=preuredite stranice +pdfOrganiser.header=Organizator PDF stranica +pdfOrganiser.submit=Preuredi stranice pdfOrganiser.mode=Način rada pdfOrganiser.mode.1=Prilagođeni redoslijed stranica pdfOrganiser.mode.2=Obrnuti redoslijed pdfOrganiser.mode.3=Duplex sortiranje -pdfOrganiser.mode.4=Booklet sortiranje -pdfOrganiser.mode.5=Knjižica s bočnim ubodom -pdfOrganiser.mode.6=Par-Nepar Podjela -pdfOrganiser.mode.7=Ukloni Prvu -pdfOrganiser.mode.8=Ukloni Zadnju -pdfOrganiser.mode.9=Ukloni Prvu i Zadnju -pdfOrganiser.mode.10=Neparno-parna kombinacija -pdfOrganiser.mode.11=Duplicate all pages +pdfOrganiser.mode.4=Sortiranje knjižice +pdfOrganiser.mode.5=Sortiranje knjižice s bočnim šivanjem +pdfOrganiser.mode.6=Par-nepar podjela +pdfOrganiser.mode.7=Ukloni prvu +pdfOrganiser.mode.8=Ukloni zadnju +pdfOrganiser.mode.9=Ukloni prvu i zadnju +pdfOrganiser.mode.10=Par-nepar spajanje +pdfOrganiser.mode.11=Dupliciraj sve stranice pdfOrganiser.placeholder=(npr. 1,3,2 ili 4-8,2,10-12 ili 2n-1) #multiTool -multiTool.title=PDF Višenamjenski alat -multiTool.header=PDF Višenamjenski alat +multiTool.title=PDF višenamjenski alat +multiTool.header=PDF višenamjenski alat multiTool.uploadPrompts=Naziv datoteke -multiTool.selectAll=Select All -multiTool.deselectAll=Deselect All -multiTool.selectPages=Page Select -multiTool.selectedPages=Selected Pages -multiTool.page=Page -multiTool.deleteSelected=Delete Selected -multiTool.downloadAll=Export -multiTool.downloadSelected=Export Selected +multiTool.selectAll=Odaberi sve +multiTool.deselectAll=Poništi odabir svih +multiTool.selectPages=Odabir stranica +multiTool.selectedPages=Odabrane stranice +multiTool.page=Stranica +multiTool.deleteSelected=Izbriši odabrano +multiTool.downloadAll=Izvezi +multiTool.downloadSelected=Izvezi odabrano -multiTool.insertPageBreak=Insert Page Break -multiTool.addFile=Add File -multiTool.rotateLeft=Rotate Left -multiTool.rotateRight=Rotate Right -multiTool.split=Split -multiTool.moveLeft=Move Left -multiTool.moveRight=Move Right -multiTool.delete=Delete -multiTool.dragDropMessage=Page(s) Selected -multiTool.undo=Undo -multiTool.redo=Redo +multiTool.insertPageBreak=Umetni prijelom stranice +multiTool.addFile=Dodaj datoteku +multiTool.rotateLeft=Rotiraj lijevo +multiTool.rotateRight=Rotiraj desno +multiTool.split=Podijeli +multiTool.moveLeft=Pomakni lijevo +multiTool.moveRight=Pomakni desno +multiTool.delete=Izbriši +multiTool.dragDropMessage=Odabrana(e) stranica(e) +multiTool.undo=Poništi +multiTool.redo=Ponovi #decrypt -decrypt.passwordPrompt=This file is password-protected. Please enter the password: -decrypt.cancelled=Operation cancelled for PDF: {0} -decrypt.noPassword=No password provided for encrypted PDF: {0} -decrypt.invalidPassword=Please try again with the correct password. -decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0} -decrypt.unexpectedError=There was an error processing the file. Please try again. -decrypt.serverError=Server error while decrypting: {0} -decrypt.success=File decrypted successfully. +decrypt.passwordPrompt=Ova datoteka je zaštićena lozinkom. Molimo unesite lozinku: +decrypt.cancelled=Operacija otkazana za PDF: {0} +decrypt.noPassword=Nije unesena lozinka za šifrirani PDF: {0} +decrypt.invalidPassword=Molimo pokušajte ponovo s ispravnom lozinkom. +decrypt.invalidPasswordHeader=Netočna lozinka ili nepodržana enkripcija za PDF: {0} +decrypt.unexpectedError=Došlo je do pogreške pri obradi datoteke. Molimo pokušajte ponovo. +decrypt.serverError=Greška poslužitelja tijekom dešifriranja: {0} +decrypt.success=Datoteka uspješno dešifrirana. #multiTool-advert -multiTool-advert.message=This feature is also available in our multi-tool page. Check it out for enhanced page-by-page UI and additional features! +multiTool-advert.message=Ova značajka je također dostupna na našoj stranici s višenamjenskim alatom. Provjerite je za poboljšano korisničko sučelje stranicu po stranicu i dodatne značajke! #view pdf -viewPdf.title=View/Edit PDF -viewPdf.header=Pogledaj PDF +viewPdf.title=Pregled/Uređivanje PDF-a +viewPdf.header=Pregled PDF-a #pageRemover pageRemover.title=Uklanjanje stranica -pageRemover.header=Uklanjanje stranica iz PDF-a -pageRemover.pagesToDelete=Stranice za brisanje (Unesite listu brojeva stranica odvojenih zarezima) : -pageRemover.submit=Obriši stranice +pageRemover.header=Uklanjanje PDF stranica +pageRemover.pagesToDelete=Stranice za brisanje (unesite popis brojeva stranica odvojenih zarezima): +pageRemover.submit=Izbriši stranice pageRemover.placeholder=(npr. 1,2,6 ili 1-10,15-30) #rotate -rotate.title=Zakreni PDF -rotate.header=Zakreni PDF -rotate.selectAngle=Odaberite kut rotacije (u umnošcima od 90 stupnjeva): -rotate.submit=Zakreni +rotate.title=Rotiraj PDF +rotate.header=Rotiraj PDF +rotate.selectAngle=Odaberite kut rotacije (u višekratnicima od 90 stupnjeva): +rotate.submit=Rotiraj #split-pdfs -split.title=Razdvajanje PDF-a -split.header=Razdvajanje PDF-a +split.title=Podijeli PDF +split.header=Podijeli PDF split.desc.1=Brojevi koje odaberete su brojevi stranica na kojima želite napraviti podjelu -split.desc.2=s takvim odabirom 1,3,7-9 bi se dokument od 10 stranica podijelio u 6 zasebnih PDF-ova sa: +split.desc.2=Tako bi odabir 1,3,7-9 podijelio dokument od 10 stranica u 6 zasebnih PDF-ova: split.desc.3=Dokument #1: Stranica 1 split.desc.4=Dokument #2: Stranice 2 i 3 -split.desc.5=Dokument #3: Stranice 4, 5, 6 i 7 +split.desc.5=Dokument #3: Stranice 4, 5, 6, 7 split.desc.6=Dokument #4: Stranica 8 split.desc.7=Dokument #5: Stranica 9 -split.desc.8=Dokument #6: Stranice 10 -split.splitPages=Unesite stranice za razdvajanje: -split.submit=Razdvoji +split.desc.8=Dokument #6: Stranica 10 +split.splitPages=Unesite stranice za podjelu: +split.submit=Podijeli #merge @@ -1417,47 +1442,76 @@ imageToPDF.submit=Pretvori imageToPDF.selectLabel=Opcije prilagodbe slike imageToPDF.fillPage=Ispuni stranicu imageToPDF.fitDocumentToImage=Prilagodi stranicu slici -imageToPDF.maintainAspectRatio=Sačuvaj omjere slike -imageToPDF.selectText.2=Automatsko zaktretanje PDF-a +imageToPDF.maintainAspectRatio=Zadrži omjer slike +imageToPDF.selectText.2=Automatsko rotiranje PDF-a imageToPDF.selectText.3=Logika više datoteka (omogućeno samo ako radite s više slika) -imageToPDF.selectText.4=Spojite u jedan PDF +imageToPDF.selectText.4=Spoji u jedan PDF imageToPDF.selectText.5=Pretvori u zasebne PDF-ove +#cbzToPDF +cbzToPDF.title=CBZ to PDF +cbzToPDF.header=CBZ to PDF +cbzToPDF.submit=Convert to PDF +cbzToPDF.selectText=Select CBZ file +cbzToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBZ +pdfToCBZ.title=PDF to CBZ +pdfToCBZ.header=PDF to CBZ +pdfToCBZ.submit=Convert to CBZ +pdfToCBZ.selectText=Select PDF file +pdfToCBZ.dpi=DPI (Dots Per Inch) + +#cbrToPDF +cbrToPDF.title=CBR to PDF +cbrToPDF.header=CBR to PDF +cbrToPDF.submit=Convert to PDF +cbrToPDF.selectText=Select CBR file +cbrToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBR +pdfToCBR.title=PDF to CBR +pdfToCBR.header=PDF to CBR +pdfToCBR.submit=Convert to CBR +pdfToCBR.selectText=Select PDF file +pdfToCBR.dpi=DPI (Dots Per Inch) +pdfToCBR.dpiHelp=Higher DPI results in better quality but larger file size. #pdfToImage pdfToImage.title=PDF u sliku pdfToImage.header=PDF u sliku pdfToImage.selectText=Format slike -pdfToImage.singleOrMultiple=Vrsta rezultata Stranica u sliku -pdfToImage.single=Jedna velika slika koja sadrži sve stranice -pdfToImage.multi=Više slika, jedna slika po stranici +pdfToImage.singleOrMultiple=Vrsta rezultata slike +pdfToImage.single=Jedna velika slika +pdfToImage.multi=Više slika pdfToImage.colorType=Tip boje pdfToImage.color=Boja pdfToImage.grey=Sivi tonovi pdfToImage.blackwhite=Crno-bijelo (mogu se izgubiti podaci!) -pdfToImage.dpi=DPI (The server limit is {0} dpi) +pdfToImage.dpi=DPI (ograničenje poslužitelja je {0} dpi) pdfToImage.submit=Pretvori -pdfToImage.info=Python nije instaliran. Treba je za konverziju na WebP. -pdfToImage.placeholder=(t.j. 1,2,8 ili 4,7,12-16 ili 2n-1) +pdfToImage.info=Python nije instaliran. Potreban je za WebP pretvorbu. +pdfToImage.placeholder=(npr. 1,2,8 ili 4,7,12-16 ili 2n-1) +pdfToImage.includeAnnotations=Include annotations (comments, highlights etc.) #addPassword -addPassword.title=Dodajte zaporku -addPassword.header=Dodajte zaporku (kriptiraj) +addPassword.title=Dodaj lozinku +addPassword.header=Dodaj lozinku (šifriraj) addPassword.selectText.1=Odaberite PDF za šifriranje -addPassword.selectText.2=Korisnička Zaporka +addPassword.selectText.2=Korisnička lozinka addPassword.selectText.3=Dužina ključa šifriranja -addPassword.selectText.4=Više vrijednosti su jače, ali niže vrijednosti imaju bolju kompatibilnost. +addPassword.selectText.4=Veće vrijednosti su jače, ali niže vrijednosti imaju bolju kompatibilnost. addPassword.selectText.5=Dopuštenja za postavljanje (preporučuje se korištenje uz vlasničku lozinku) -addPassword.selectText.6=Spriječiti sastavljanje dokumenta -addPassword.selectText.7=Spriječite izdvajanje sadržaja -addPassword.selectText.8=Spriječite izvlačenje radi pristupačnosti -addPassword.selectText.9=Spriječiti ispunjavanje obrasca -addPassword.selectText.10=Spriječiti izmjene -addPassword.selectText.11=Spriječi modificiranje napomena -addPassword.selectText.12=Spriječiti ispis -addPassword.selectText.13=Spriječite ispis različitih formata -addPassword.selectText.14=Zaporka vlasnika +addPassword.selectText.6=Spriječi sastavljanje dokumenta +addPassword.selectText.7=Spriječi izdvajanje sadržaja +addPassword.selectText.8=Spriječi izdvajanje radi pristupačnosti +addPassword.selectText.9=Spriječi ispunjavanje obrasca +addPassword.selectText.10=Spriječi izmjene +addPassword.selectText.11=Spriječi izmjenu bilješki +addPassword.selectText.12=Spriječi ispis +addPassword.selectText.13=Spriječi ispis različitih formata +addPassword.selectText.14=Vlasnička lozinka addPassword.selectText.15=Ograničava što se može učiniti s dokumentom nakon što se otvori (ne podržavaju svi čitači) addPassword.selectText.16=Ograničava otvaranje samog dokumenta addPassword.submit=Šifriraj @@ -1471,46 +1525,46 @@ watermark.selectText.1=Izaberite PDF za dodavanje vodenog žiga: watermark.selectText.2=Tekst vodenog žiga: watermark.selectText.3=Veličina fonta: watermark.selectText.4=Rotacija (0-360): -watermark.selectText.5=Širina razmaka (Razmak između svakog vodenog žiga vodoravno): -watermark.selectText.6=Visina razmaka (Razmak između svakog vodenog žiga okomito): +watermark.selectText.5=Razmak širine (vodoravni razmak između vodenih žigova): +watermark.selectText.6=Razmak visine (okomiti razmak između vodenih žigova): watermark.selectText.7=Neprozirnost (0% - 100%): watermark.selectText.8=Vrsta vodenog žiga: watermark.selectText.9=Slika vodenog žiga: -watermark.selectText.10=Konvertiraj PDF u PDF-Sliku +watermark.selectText.10=Pretvori PDF u PDF-sliku watermark.submit=Dodaj vodeni žig watermark.type.1=Tekst watermark.type.2=Slika #Change permissions -permissions.title=Promjena dopuštenja -permissions.header=Promjena dopuštenja -permissions.warning=Upozorenje: da ove dozvole budu nepromjenjive, preporuča se da ih postavite lozinkom putem stranice za dodavanje lozinke +permissions.title=Promijeni dopuštenja +permissions.header=Promijeni dopuštenja +permissions.warning=Upozorenje: da bi ova dopuštenja bila nepromjenjiva, preporučuje se da ih postavite lozinkom putem stranice za dodavanje lozinke permissions.selectText.1=Odaberite PDF za promjenu dopuštenja permissions.selectText.2=Dopuštenja za postavljanje -permissions.selectText.3=Spriječiti sastavljanje dokumenta -permissions.selectText.4=Spriječiti izdvajanje sadržaja -permissions.selectText.5=Spriječite izvlačenje radi pristupačnosti -permissions.selectText.6=Spriječiti ispunjavanje obrasca -permissions.selectText.7=Spriječiti izmjene -permissions.selectText.8=Spriječi modificiranje napomena -permissions.selectText.9=Spriječiti ispis -permissions.selectText.10=Spriječite ispis različitih formata -permissions.submit=Promijeniti +permissions.selectText.3=Spriječi sastavljanje dokumenta +permissions.selectText.4=Spriječi izdvajanje sadržaja +permissions.selectText.5=Spriječi izdvajanje radi pristupačnosti +permissions.selectText.6=Spriječi ispunjavanje obrasca +permissions.selectText.7=Spriječi izmjene +permissions.selectText.8=Spriječi izmjenu bilješki +permissions.selectText.9=Spriječi ispis +permissions.selectText.10=Spriječi ispis različitih formata +permissions.submit=Promijeni #remove password -removePassword.title=Ukloni zaporku -removePassword.header=Ukloni zaporku (dekriptiraj) -removePassword.selectText.1=Odaberite PDF za dekriptiranje -removePassword.selectText.2=Zaporka -removePassword.submit=Ukloniti +removePassword.title=Ukloni lozinku +removePassword.header=Ukloni lozinku (dešifriraj) +removePassword.selectText.1=Odaberite PDF za dešifriranje +removePassword.selectText.2=Lozinka +removePassword.submit=Ukloni #changeMetadata -changeMetadata.title=Promjena metapodataka -changeMetadata.header=Promjena metapodataka -changeMetadata.selectText.1=Uredite varijable koje želite promijeniti +changeMetadata.title=Naslov: +changeMetadata.header=Promijeni metapodatke +changeMetadata.selectText.1=Molimo uredite varijable koje želite promijeniti changeMetadata.selectText.2=Izbriši sve metapodatke changeMetadata.selectText.3=Prikaži prilagođene metapodatke: changeMetadata.author=Autor: @@ -1520,95 +1574,95 @@ changeMetadata.keywords=Ključne riječi: changeMetadata.modDate=Datum izmjene (gggg/MM/dd HH:mm:ss): changeMetadata.producer=Proizvođač: changeMetadata.subject=Predmet: -changeMetadata.trapped=Zarobljen: +changeMetadata.trapped=Zarobljeno: changeMetadata.selectText.4=Ostali metapodaci: changeMetadata.selectText.5=Dodaj prilagođeni unos metapodataka -changeMetadata.submit=Promijeniti +changeMetadata.submit=Promijeni #unlockPDFForms -unlockPDFForms.title=Remove Read-Only from Form Fields -unlockPDFForms.header=Unlock PDF Forms -unlockPDFForms.submit=Remove +unlockPDFForms.title=Ukloni samo za čitanje iz polja obrasca +unlockPDFForms.header=Otključaj PDF obrasce +unlockPDFForms.submit=Ukloni #pdfToPDFA pdfToPDFA.title=PDF u PDF/A pdfToPDFA.header=PDF u PDF/A -pdfToPDFA.credit=Ova usluga koristi libreoffice za PDF/A pretvorbu -pdfToPDFA.submit=Pretvoriti +pdfToPDFA.credit=Ova usluga koristi LibreOffice za PDF/A pretvorbu +pdfToPDFA.submit=Pretvori pdfToPDFA.tip=Trenutno ne radi za više unosa odjednom pdfToPDFA.outputFormat=Izlazni format -pdfToPDFA.pdfWithDigitalSignature=PDF sadrži digitalni potpis. U sledećem koraku će biti uklonjen. +pdfToPDFA.pdfWithDigitalSignature=PDF sadrži digitalni potpis. On će biti uklonjen u sljedećem koraku. #PDFToWord PDFToWord.title=PDF u Word PDFToWord.header=PDF u Word PDFToWord.selectText.1=Format izlazne datoteke -PDFToWord.credit=Ova usluga koristi LibreOffice za konverziju datoteka. -PDFToWord.submit=Pretvoriti +PDFToWord.credit=Ova usluga koristi LibreOffice za pretvorbu datoteka. +PDFToWord.submit=Pretvori #PDFToPresentation -PDFToPresentation.title=PDF u Prezentaciju -PDFToPresentation.header=PDF u Prezentaciju +PDFToPresentation.title=PDF u prezentaciju +PDFToPresentation.header=PDF u prezentaciju PDFToPresentation.selectText.1=Format izlazne datoteke -PDFToPresentation.credit=Ova usluga koristi LibreOffice za konverziju datoteka. -PDFToPresentation.submit=Pretvoriti +PDFToPresentation.credit=Ova usluga koristi LibreOffice za pretvorbu datoteka. +PDFToPresentation.submit=Pretvori #PDFToText -PDFToText.title=PDF u RTF (Tekst) -PDFToText.header=PDF u RTF (Tekst) +PDFToText.title=PDF u RTF (tekst) +PDFToText.header=PDF u RTF (tekst) PDFToText.selectText.1=Format izlazne datoteke -PDFToText.credit=Ova usluga koristi LibreOffice za konverziju datoteka. -PDFToText.submit=Pretvoriti +PDFToText.credit=Ova usluga koristi LibreOffice za pretvorbu datoteka. +PDFToText.submit=Pretvori #PDFToHTML PDFToHTML.title=PDF u HTML PDFToHTML.header=PDF u HTML -PDFToHTML.credit=Ova usluga koristi pdftohtml za konverziju datoteka. -PDFToHTML.submit=Pretvoriti +PDFToHTML.credit=Ova usluga koristi pdftohtml za pretvorbu datoteka. +PDFToHTML.submit=Pretvori #PDFToXML PDFToXML.title=PDF u XML PDFToXML.header=PDF u XML -PDFToXML.credit=Ova usluga koristi LibreOffice za konverziju datoteka. -PDFToXML.submit=Pretvoriti +PDFToXML.credit=Ova usluga koristi LibreOffice za pretvorbu datoteka. +PDFToXML.submit=Pretvori #PDFToCSV PDFToCSV.title=PDF u CSV PDFToCSV.header=PDF u CSV PDFToCSV.prompt=Odaberite stranicu za izdvajanje tablice -PDFToCSV.submit=Izvuci +PDFToCSV.submit=Izdvoji #split-by-size-or-count -split-by-size-or-count.title=Podijeli PDF prema veličini ili broju -split-by-size-or-count.header=Podijeli PDF prema veličini ili broju +split-by-size-or-count.title=Podijeli PDF po veličini ili broju +split-by-size-or-count.header=Podijeli PDF po veličini ili broju split-by-size-or-count.type.label=Odaberite vrstu dijeljenja split-by-size-or-count.type.size=Po veličini split-by-size-or-count.type.pageCount=Po broju stranica split-by-size-or-count.type.docCount=Po broju dokumenata split-by-size-or-count.value.label=Unesite vrijednost split-by-size-or-count.value.placeholder=Unesite veličinu (npr. 2MB ili 3KB) ili broj (npr. 5) -split-by-size-or-count.submit=Potvrdite +split-by-size-or-count.submit=Pošalji #overlay-pdfs -overlay-pdfs.header=Prekrivanje PDF datoteka -overlay-pdfs.baseFile.label=Odaberite Osnovnu PDF datoteka -overlay-pdfs.overlayFiles.label=Izaberite PDF datoteke za prekrivanje +overlay-pdfs.header=Preklopi PDF datoteke +overlay-pdfs.baseFile.label=Odaberite osnovnu PDF datoteku +overlay-pdfs.overlayFiles.label=Odaberite PDF datoteke za preklapanje overlay-pdfs.mode.label=Odaberite način preklapanja overlay-pdfs.mode.sequential=Sekvencijalno preklapanje -overlay-pdfs.mode.interleaved=Isprepleteni sloj -overlay-pdfs.mode.fixedRepeat=Popravljeni sloj ponavljanja -overlay-pdfs.counts.label=Brojevi preklapanja (za način fiksnog ponavljanja) +overlay-pdfs.mode.interleaved=Isprepleteno preklapanje +overlay-pdfs.mode.fixedRepeat=Fiksno ponavljajuće preklapanje +overlay-pdfs.counts.label=Broj preklapanja (za način fiksnog ponavljanja) overlay-pdfs.counts.placeholder=Unesite brojeve odvojene zarezima (npr. 2,3,1) overlay-pdfs.position.label=Odaberite položaj preklapanja overlay-pdfs.position.foreground=Prednji plan overlay-pdfs.position.background=Pozadina -overlay-pdfs.submit=Potvrditi +overlay-pdfs.submit=Pošalji #split-by-sections @@ -1618,57 +1672,57 @@ split-by-sections.horizontal.label=Vodoravne podjele split-by-sections.vertical.label=Okomite podjele split-by-sections.horizontal.placeholder=Unesite broj vodoravnih podjela split-by-sections.vertical.placeholder=Unesite broj okomitih podjela -split-by-sections.submit=Razdvojiti PDF +split-by-sections.submit=Podijeli PDF split-by-sections.merge=Spoji u jedan PDF #printFile printFile.title=Ispis datoteke printFile.header=Ispis datoteke na pisač -printFile.selectText.1=Odaberite Datoteku za ispis +printFile.selectText.1=Odaberite datoteku za ispis printFile.selectText.2=Unesite naziv pisača -printFile.submit=Ispis +printFile.submit=Ispiši #licenses licenses.nav=Licence -licenses.title=Licence treće strane -licenses.header=Licence treće strane +licenses.title=Licence trećih strana +licenses.header=Licence trećih strana licenses.module=Modul licenses.version=Verzija licenses.license=Licenca #survey -survey.nav=Upitnica -survey.title=Stirling-PDF Upitnica -survey.description=Stirling-PDF nema praćenje pa želimo svesnost korisnika da bi poboljšali Stirling-PDF! -survey.changes=Stirling-PDF je promenjen od poslednje upitnice! Za više informacija, proverite naš blog ovdje: -survey.changes2=S ovim promenama dobivamo platnu podršku i financiranje poslovnim aktivnostima -survey.please=Please consider taking our survey! -survey.disabled=(Upitnica popup će biti onemogućena u sljedećim ažuracanjima aliće se nalaziti na dnu stranice) -survey.button=Izvrsi upitnicu -survey.dontShowAgain=Ne prikazujući ponovo -survey.meeting.1=If you're using Stirling PDF at work, we'd love to speak to you. We're offering technical support sessions in exchange for a 15 minute user discovery session. -survey.meeting.2=This is a chance to: -survey.meeting.3=Get help with deployment, integrations, or troubleshooting -survey.meeting.4=Provide direct feedback on performance, edge cases, and feature gaps -survey.meeting.5=Help us refine Stirling PDF for real-world enterprise use -survey.meeting.6=If you're interested, you can book time with our team directly. (English speaking only) -survey.meeting.7=Looking forward to digging into your use cases and making Stirling PDF even better! -survey.meeting.notInterested=Not a business and/or interested in a meeting? -survey.meeting.button=Book meeting +survey.nav=Anketa +survey.title=Stirling-PDF anketa +survey.description=Stirling-PDF nema praćenje pa želimo čuti od naših korisnika kako bismo poboljšali Stirling-PDF! +survey.changes=Stirling-PDF se promijenio od zadnje ankete! Da biste saznali više, provjerite naš blog post ovdje: +survey.changes2=S ovim promjenama dobivamo plaćenu poslovnu podršku i financiranje +survey.please=Molimo razmislite o sudjelovanju u našoj anketi! +survey.disabled=(Skočni prozor ankete bit će onemogućen u sljedećim ažuriranjima, ali će biti dostupan na dnu stranice) +survey.button=Ispuni anketu +survey.dontShowAgain=Ne prikazuj ponovo +survey.meeting.1=Ako koristite Stirling PDF na poslu, voljeli bismo razgovarati s vama. Nudimo sesije tehničke podrške u zamjenu za 15-minutnu sesiju otkrivanja korisnika. +survey.meeting.2=Ovo je prilika da: +survey.meeting.3=Dobijete pomoć s implementacijom, integracijama ili rješavanjem problema +survey.meeting.4=Pružite izravne povratne informacije o performansama, rubnim slučajevima i nedostacima značajki +survey.meeting.5=Pomognete nam poboljšati Stirling PDF za stvarnu poslovnu upotrebu +survey.meeting.6=Ako ste zainteresirani, možete izravno rezervirati vrijeme s našim timom (samo na engleskom jeziku). +survey.meeting.7=Radujemo se što ćemo istražiti vaše slučajeve upotrebe i učiniti Stirling PDF još boljim! +survey.meeting.notInterested=Niste poslovni korisnik i/ili niste zainteresirani za sastanak? +survey.meeting.button=Zakaži sastanak #error -error.sorry=Oprostite zbog problema! +error.sorry=Žao nam je zbog problema! error.needHelp=Trebate pomoć / Pronašli ste problem? -error.contactTip=Ako i dalje imate problema, ne ustručavajte se obratiti nam se za pomoć. Tiket možete poslati na našoj GitHub stranici ili nas kontaktirati putem Discorda: +error.contactTip=Ako i dalje imate problema, ne ustručavajte se obratiti nam se za pomoć. Možete poslati prijavu na našoj GitHub stranici ili nas kontaktirati putem Discorda: error.404.head=404 - Stranica nije pronađena | Ups, spotaknuli smo se u kodu! error.404.1=Čini se da ne možemo pronaći stranicu koju tražite. error.404.2=Nešto je pošlo po zlu -error.github=Pošaljite ticket na GitHub -error.showStack=Prikaži Stack Trace -error.copyStack=Kopiraj Stack Trace -error.githubSubmit=GitHub - Pošaljite ticket +error.github=Pošaljite prijavu na GitHub +error.showStack=Prikaži trag stogova +error.copyStack=Kopiraj trag stogova +error.githubSubmit=GitHub - Pošaljite prijavu error.discordSubmit=Discord - Pošalji objavu podrške @@ -1676,230 +1730,230 @@ error.discordSubmit=Discord - Pošalji objavu podrške removeImage.title=Ukloni sliku removeImage.header=Ukloni sliku removeImage.removeImage=Ukloni sliku -removeImage.submit=Izbriši sliku +removeImage.submit=Ukloni sliku -splitByChapters.title=Podijeli PDF naoglazdene glave -splitByChapters.header=Podijeli PDF naoglazdene glave -splitByChapters.bookmarkLevel=Nivo oznaka +splitByChapters.title=Podijeli PDF po poglavljima +splitByChapters.header=Podijeli PDF po poglavljima +splitByChapters.bookmarkLevel=Razina oznake splitByChapters.includeMetadata=Uključi metapodatke -splitByChapters.allowDuplicates=Dopuštaj duplikate -splitByChapters.desc.1=Ova alatka podijeli PDF datoteku u više PDFa na teme njene strukture glava. -splitByChapters.desc.2=Nivo oznaka: Odaberite nivo oznaka koji će se koristiti za podjelu (0 za prvi nivo, 1 za drugi nivo itd.). -splitByChapters.desc.3=Uključi metapodatke: Ako je pokušano, metapodaci iz originalne PDF datoteke će biti uključeni u svaku podijeljenu PDF datoteku. -splitByChapters.desc.4=Dopuštaj duplikate: Ako je ova opcija zaštićena, dozvoljava se da se na istoj strani mogu stvoriti posebne PDF datoteke s više oznaka. +splitByChapters.allowDuplicates=Dopusti duplikate +splitByChapters.desc.1=Ovaj alat dijeli PDF datoteku u više PDF-ova na temelju njezine strukture poglavlja. +splitByChapters.desc.2=Razina oznake: Odaberite razinu oznaka koje će se koristiti za dijeljenje (0 za najvišu razinu, 1 za drugu razinu itd.). +splitByChapters.desc.3=Uključi metapodatke: Ako je označeno, metapodaci izvornog PDF-a bit će uključeni u svaki podijeljeni PDF. +splitByChapters.desc.4=Dopusti duplikate: Ako je označeno, omogućuje stvaranje zasebnih PDF-ova s više oznaka na istoj stranici. splitByChapters.submit=Podijeli PDF #File Chooser -fileChooser.click=Click -fileChooser.or=or -fileChooser.dragAndDrop=Drag & Drop -fileChooser.dragAndDropPDF=Drag & Drop PDF file -fileChooser.dragAndDropImage=Drag & Drop Image file -fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here -fileChooser.extractPDF=Extracting... -fileChooser.addAttachments=drag & drop attachments here +fileChooser.click=Kliknite +fileChooser.or=ili +fileChooser.dragAndDrop=Povuci i ispusti +fileChooser.dragAndDropPDF=Povuci i ispusti PDF datoteku +fileChooser.dragAndDropImage=Povuci i ispusti slikovnu datoteku +fileChooser.hoveredDragAndDrop=Povuci i ispusti datoteku(e) ovdje +fileChooser.extractPDF=Izdvajanje... +fileChooser.addAttachments=povuci i ispusti privitke ovdje #release notes -releases.footer=Releases -releases.title=Release Notes -releases.header=Release Notes -releases.current.version=Current Release -releases.note=Release notes are only available in English +releases.footer=Izdanja +releases.title=Bilješke o izdanju +releases.header=Bilješke o izdanju +releases.current.version=Trenutno izdanje +releases.note=Bilješke o izdanju dostupne su samo na engleskom jeziku #Validate Signature -validateSignature.title=Validate PDF Signatures -validateSignature.header=Validate Digital Signatures -validateSignature.selectPDF=Select signed PDF file -validateSignature.submit=Validate Signatures -validateSignature.results=Validation Results +validateSignature.title=Provjeri PDF potpise +validateSignature.header=Provjeri digitalne potpise +validateSignature.selectPDF=Odaberite potpisanu PDF datoteku +validateSignature.submit=Provjeri potpise +validateSignature.results=Rezultati provjere validateSignature.status=Status -validateSignature.signer=Signer -validateSignature.date=Date -validateSignature.reason=Reason -validateSignature.location=Location -validateSignature.noSignatures=No digital signatures found in this document -validateSignature.status.valid=Valid -validateSignature.status.invalid=Invalid -validateSignature.chain.invalid=Certificate chain validation failed - cannot verify signer's identity -validateSignature.trust.invalid=Certificate not in trust store - source cannot be verified -validateSignature.cert.expired=Certificate has expired -validateSignature.cert.revoked=Certificate has been revoked -validateSignature.signature.info=Signature Information -validateSignature.signature=Signature -validateSignature.signature.mathValid=Signature is mathematically valid BUT: -validateSignature.selectCustomCert=Custom Certificate File X.509 (Optional) -validateSignature.cert.info=Certificate Details -validateSignature.cert.issuer=Issuer -validateSignature.cert.subject=Subject -validateSignature.cert.serialNumber=Serial Number -validateSignature.cert.validFrom=Valid From -validateSignature.cert.validUntil=Valid Until -validateSignature.cert.algorithm=Algorithm -validateSignature.cert.keySize=Key Size -validateSignature.cert.version=Version -validateSignature.cert.keyUsage=Key Usage -validateSignature.cert.selfSigned=Self-Signed -validateSignature.cert.bits=bits +validateSignature.signer=Potpisnik +validateSignature.date=Datum +validateSignature.reason=Razlog +validateSignature.location=Lokacija +validateSignature.noSignatures=Nema digitalnih potpisa u ovom dokumentu +validateSignature.status.valid=Valjano +validateSignature.status.invalid=Nevaljano +validateSignature.chain.invalid=Provjera lanca certifikata nije uspjela - nije moguće provjeriti identitet potpisnika +validateSignature.trust.invalid=Certifikat nije u spremištu povjerenja - izvor se ne može provjeriti +validateSignature.cert.expired=Certifikat je istekao +validateSignature.cert.revoked=Certifikat je opozvan +validateSignature.signature.info=Informacije o potpisu +validateSignature.signature=Potpis +validateSignature.signature.mathValid=Potpis je matematički valjan, ALI: +validateSignature.selectCustomCert=Prilagođena datoteka certifikata X.509 (neobavezno) +validateSignature.cert.info=Detalji certifikata +validateSignature.cert.issuer=Izdavatelj +validateSignature.cert.subject=Predmet +validateSignature.cert.serialNumber=Serijski broj +validateSignature.cert.validFrom=Vrijedi od +validateSignature.cert.validUntil=Vrijedi do +validateSignature.cert.algorithm=Algoritam +validateSignature.cert.keySize=Veličina ključa +validateSignature.cert.version=Verzija +validateSignature.cert.keyUsage=Upotreba ključa +validateSignature.cert.selfSigned=Samopotpisan +validateSignature.cert.bits=bita # Audit Dashboard -audit.dashboard.title=Audit Dashboard -audit.dashboard.systemStatus=Audit System Status +audit.dashboard.title=Nadzorna ploča revizije +audit.dashboard.systemStatus=Status sustava revizije audit.dashboard.status=Status -audit.dashboard.enabled=Enabled -audit.dashboard.disabled=Disabled -audit.dashboard.currentLevel=Current Level -audit.dashboard.retentionPeriod=Retention Period -audit.dashboard.days=days -audit.dashboard.totalEvents=Total Events +audit.dashboard.enabled=Omogućeno +audit.dashboard.disabled=Onemogućeno +audit.dashboard.currentLevel=Trenutna razina +audit.dashboard.retentionPeriod=Razdoblje zadržavanja +audit.dashboard.days=dana +audit.dashboard.totalEvents=Ukupno događaja # Audit Dashboard Tabs -audit.dashboard.tab.dashboard=Dashboard -audit.dashboard.tab.events=Audit Events -audit.dashboard.tab.export=Export +audit.dashboard.tab.dashboard=Nadzorna ploča +audit.dashboard.tab.events=Događaji revizije +audit.dashboard.tab.export=Izvoz # Dashboard Charts -audit.dashboard.eventsByType=Events by Type -audit.dashboard.eventsByUser=Events by User -audit.dashboard.eventsOverTime=Events Over Time -audit.dashboard.period.7days=7 Days -audit.dashboard.period.30days=30 Days -audit.dashboard.period.90days=90 Days +audit.dashboard.eventsByType=Događaji po vrsti +audit.dashboard.eventsByUser=Događaji po korisniku +audit.dashboard.eventsOverTime=Događaji tijekom vremena +audit.dashboard.period.7days=7 dana +audit.dashboard.period.30days=30 dana +audit.dashboard.period.90days=90 dana # Events Tab -audit.dashboard.auditEvents=Audit Events -audit.dashboard.filter.eventType=Event Type -audit.dashboard.filter.allEventTypes=All event types -audit.dashboard.filter.user=User -audit.dashboard.filter.userPlaceholder=Filter by user -audit.dashboard.filter.startDate=Start Date -audit.dashboard.filter.endDate=End Date -audit.dashboard.filter.apply=Apply Filters -audit.dashboard.filter.reset=Reset Filters +audit.dashboard.auditEvents=Događaji revizije +audit.dashboard.filter.eventType=Vrsta događaja +audit.dashboard.filter.allEventTypes=Sve vrste događaja +audit.dashboard.filter.user=Korisnik +audit.dashboard.filter.userPlaceholder=Filtriraj po korisniku +audit.dashboard.filter.startDate=Datum početka +audit.dashboard.filter.endDate=Datum završetka +audit.dashboard.filter.apply=Primijeni filtre +audit.dashboard.filter.reset=Poništi filtre # Table Headers audit.dashboard.table.id=ID -audit.dashboard.table.time=Time -audit.dashboard.table.user=User -audit.dashboard.table.type=Type -audit.dashboard.table.details=Details -audit.dashboard.table.viewDetails=View Details +audit.dashboard.table.time=Vrijeme +audit.dashboard.table.user=Korisnik +audit.dashboard.table.type=Vrsta +audit.dashboard.table.details=Detalji +audit.dashboard.table.viewDetails=Prikaži detalje # Pagination -audit.dashboard.pagination.show=Show -audit.dashboard.pagination.entries=entries -audit.dashboard.pagination.pageInfo1=Page -audit.dashboard.pagination.pageInfo2=of -audit.dashboard.pagination.totalRecords=Total records: +audit.dashboard.pagination.show=Prikaži +audit.dashboard.pagination.entries=unosa +audit.dashboard.pagination.pageInfo1=Stranica +audit.dashboard.pagination.pageInfo2=od +audit.dashboard.pagination.totalRecords=Ukupno zapisa: # Modal -audit.dashboard.modal.eventDetails=Event Details +audit.dashboard.modal.eventDetails=Detalji događaja audit.dashboard.modal.id=ID -audit.dashboard.modal.user=User -audit.dashboard.modal.type=Type -audit.dashboard.modal.time=Time -audit.dashboard.modal.data=Data +audit.dashboard.modal.user=Korisnik +audit.dashboard.modal.type=Vrsta +audit.dashboard.modal.time=Vrijeme +audit.dashboard.modal.data=Podaci # Export Tab -audit.dashboard.export.title=Export Audit Data -audit.dashboard.export.format=Export Format -audit.dashboard.export.csv=CSV (Comma Separated Values) +audit.dashboard.export.title=Izvoz podataka revizije +audit.dashboard.export.format=Format izvoza +audit.dashboard.export.csv=CSV (vrijednosti odvojene zarezima) audit.dashboard.export.json=JSON (JavaScript Object Notation) -audit.dashboard.export.button=Export Data -audit.dashboard.export.infoTitle=Export Information -audit.dashboard.export.infoDesc1=The export will include all audit events matching the selected filters. For large datasets, the export may take a few moments to generate. -audit.dashboard.export.infoDesc2=Exported data will include: -audit.dashboard.export.infoItem1=Event ID -audit.dashboard.export.infoItem2=User -audit.dashboard.export.infoItem3=Event Type -audit.dashboard.export.infoItem4=Timestamp -audit.dashboard.export.infoItem5=Event Data +audit.dashboard.export.button=Izvezi podatke +audit.dashboard.export.infoTitle=Informacije o izvozu +audit.dashboard.export.infoDesc1=Izvoz će uključivati sve događaje revizije koji odgovaraju odabranim filtrima. Za velike skupove podataka, generiranje izvoza može potrajati nekoliko trenutaka. +audit.dashboard.export.infoDesc2=Izvezeni podaci će uključivati: +audit.dashboard.export.infoItem1=ID događaja +audit.dashboard.export.infoItem2=Korisnik +audit.dashboard.export.infoItem3=Vrsta događaja +audit.dashboard.export.infoItem4=Vremenska oznaka +audit.dashboard.export.infoItem5=Podaci o događaju # JavaScript i18n keys -audit.dashboard.js.noEventsFound=No audit events found matching the current filters -audit.dashboard.js.errorLoading=Error loading data: -audit.dashboard.js.errorRendering=Error rendering table: -audit.dashboard.js.loadingPage=Loading page +audit.dashboard.js.noEventsFound=Nema pronađenih događaja revizije koji odgovaraju trenutnim filtrima +audit.dashboard.js.errorLoading=Greška pri učitavanju podataka: +audit.dashboard.js.errorRendering=Greška pri renderiranju tablice: +audit.dashboard.js.loadingPage=Učitavanje stranice #################### # Cookie banner # #################### -cookieBanner.popUp.title=How we use Cookies -cookieBanner.popUp.description.1=We use cookies and other technologies to make Stirling PDF work better for you—helping us improve our tools and keep building features you'll love. -cookieBanner.popUp.description.2=If you’d rather not, clicking 'No Thanks' will only enable the essential cookies needed to keep things running smoothly. -cookieBanner.popUp.acceptAllBtn=Okay -cookieBanner.popUp.acceptNecessaryBtn=No Thanks -cookieBanner.popUp.showPreferencesBtn=Manage preferences -cookieBanner.preferencesModal.title=Consent Preferences Center -cookieBanner.preferencesModal.acceptAllBtn=Accept all -cookieBanner.preferencesModal.acceptNecessaryBtn=Reject all -cookieBanner.preferencesModal.savePreferencesBtn=Save preferences -cookieBanner.preferencesModal.closeIconLabel=Close modal -cookieBanner.preferencesModal.serviceCounterLabel=Service|Services -cookieBanner.preferencesModal.subtitle=Cookie Usage -cookieBanner.preferencesModal.description.1=Stirling PDF uses cookies and similar technologies to enhance your experience and understand how our tools are used. This helps us improve performance, develop the features you care about, and provide ongoing support to our users. -cookieBanner.preferencesModal.description.2=Stirling PDF cannot—and will never—track or access the content of the documents you use. -cookieBanner.preferencesModal.description.3=Your privacy and trust are at the core of what we do. -cookieBanner.preferencesModal.necessary.title.1=Strictly Necessary Cookies -cookieBanner.preferencesModal.necessary.title.2=Always Enabled -cookieBanner.preferencesModal.necessary.description=These cookies are essential for the website to function properly. They enable core features like setting your privacy preferences, logging in, and filling out forms—which is why they can’t be turned off. -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.popUp.title=Kako koristimo kolačiće +cookieBanner.popUp.description.1=Koristimo kolačiće i druge tehnologije kako bismo Stirling PDF učinili boljim za vas—pomažući nam da poboljšamo naše alate i nastavimo graditi značajke koje ćete voljeti. +cookieBanner.popUp.description.2=Ako ne želite, klikom na 'Ne, hvala' omogućit ćete samo bitne kolačiće potrebne za nesmetan rad. +cookieBanner.popUp.acceptAllBtn=U redu +cookieBanner.popUp.acceptNecessaryBtn=Ne, hvala +cookieBanner.popUp.showPreferencesBtn=Upravljaj preferencijama +cookieBanner.preferencesModal.title=Centar za postavke pristanka +cookieBanner.preferencesModal.acceptAllBtn=Prihvati sve +cookieBanner.preferencesModal.acceptNecessaryBtn=Odbij sve +cookieBanner.preferencesModal.savePreferencesBtn=Spremi postavke +cookieBanner.preferencesModal.closeIconLabel=Zatvori modal +cookieBanner.preferencesModal.serviceCounterLabel=Usluga|Usluge +cookieBanner.preferencesModal.subtitle=Upotreba kolačića +cookieBanner.preferencesModal.description.1=Stirling PDF koristi kolačiće i slične tehnologije za poboljšanje vašeg iskustva i razumijevanje načina na koji se koriste naši alati. To nam pomaže poboljšati performanse, razviti značajke do kojih vam je stalo i pružiti stalnu podršku našim korisnicima. +cookieBanner.preferencesModal.description.2=Stirling PDF ne može—i nikada neće—pratiti ili pristupati sadržaju dokumenata koje koristite. +cookieBanner.preferencesModal.description.3=Vaša privatnost i povjerenje su u srži onoga što radimo. +cookieBanner.preferencesModal.necessary.title.1=Strogo potrebni kolačići +cookieBanner.preferencesModal.necessary.title.2=Uvijek omogućeno +cookieBanner.preferencesModal.necessary.description=Ovi kolačići su ključni za ispravno funkcioniranje web stranice. Omogućuju osnovne značajke poput postavljanja vaših postavki privatnosti, prijave i ispunjavanja obrazaca—zbog čega se ne mogu isključiti. +cookieBanner.preferencesModal.analytics.title=Analitika +cookieBanner.preferencesModal.analytics.description=Ovi kolačići nam pomažu razumjeti kako se koriste naši alati, tako da se možemo usredotočiti na izgradnju značajki koje naša zajednica najviše cijeni. Budite sigurni—Stirling PDF ne može i nikada neće pratiti sadržaj dokumenata s kojima radite. #scannerEffect -scannerEffect.title=Scanner Effect -scannerEffect.header=Scanner Effect -scannerEffect.description=Create a PDF that looks like it was scanned -scannerEffect.selectPDF=Select PDF: -scannerEffect.quality=Scan Quality -scannerEffect.quality.low=Low -scannerEffect.quality.medium=Medium -scannerEffect.quality.high=High -scannerEffect.rotation=Rotation Angle -scannerEffect.rotation.none=None -scannerEffect.rotation.slight=Slight -scannerEffect.rotation.moderate=Moderate -scannerEffect.rotation.severe=Severe -scannerEffect.submit=Create Scanner Effect +scannerEffect.title=Efekt skenera +scannerEffect.header=Efekt skenera +scannerEffect.description=Stvorite PDF koji izgleda kao da je skeniran +scannerEffect.selectPDF=Odaberite PDF: +scannerEffect.quality=Kvaliteta skeniranja +scannerEffect.quality.low=Niska +scannerEffect.quality.medium=Srednja +scannerEffect.quality.high=Visoka +scannerEffect.rotation=Kut rotacije +scannerEffect.rotation.none=Nema +scannerEffect.rotation.slight=Blaga +scannerEffect.rotation.moderate=Umjerena +scannerEffect.rotation.severe=Jaka +scannerEffect.submit=Stvori efekt skenera #home.scannerEffect -home.scannerEffect.title=Scanner Effect -home.scannerEffect.desc=Create a PDF that looks like it was scanned -scannerEffect.tags=scan,simulate,realistic,convert +home.scannerEffect.title=Efekt skenera +home.scannerEffect.desc=Stvorite PDF koji izgleda kao da je skeniran +scannerEffect.tags=skeniraj,simuliraj,realistično,pretvori # ScannerEffect advanced settings (frontend) -scannerEffect.advancedSettings=Enable Advanced Scan Settings -scannerEffect.colorspace=Colorspace -scannerEffect.colorspace.grayscale=Grayscale -scannerEffect.colorspace.color=Color -scannerEffect.border=Border (px) -scannerEffect.rotate=Base Rotation (degrees) -scannerEffect.rotateVariance=Rotation Variance (degrees) -scannerEffect.brightness=Brightness -scannerEffect.contrast=Contrast -scannerEffect.blur=Blur -scannerEffect.noise=Noise -scannerEffect.yellowish=Yellowish (simulate old paper) -scannerEffect.resolution=Resolution (DPI) +scannerEffect.advancedSettings=Omogući napredne postavke skeniranja +scannerEffect.colorspace=Prostor boja +scannerEffect.colorspace.grayscale=Sivi tonovi +scannerEffect.colorspace.color=Boja +scannerEffect.border=Obrub (px) +scannerEffect.rotate=Osnovna rotacija (stupnjevi) +scannerEffect.rotateVariance=Varijacija rotacije (stupnjevi) +scannerEffect.brightness=Svjetlina +scannerEffect.contrast=Kontrast +scannerEffect.blur=Zamućenje +scannerEffect.noise=Šum +scannerEffect.yellowish=Žućkasto (simulira stari papir) +scannerEffect.resolution=Razlučivost (DPI) # Table of Contents Feature -home.editTableOfContents.title=Edit Table of Contents -home.editTableOfContents.desc=Add or edit bookmarks and table of contents in PDF documents +home.editTableOfContents.title=Uredi sadržaj +home.editTableOfContents.desc=Dodajte ili uredite oznake i sadržaj u PDF dokumentima -editTableOfContents.tags=bookmarks,toc,navigation,index,table of contents,chapters,sections,outline -editTableOfContents.title=Edit Table of Contents -editTableOfContents.header=Add or Edit PDF Table of Contents -editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to append to existing) -editTableOfContents.editorTitle=Bookmark Editor -editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks. -editTableOfContents.addBookmark=Add New Bookmark -editTableOfContents.importBookmarksDefault=Import -editTableOfContents.importBookmarksFromJsonFile=Upload JSON file -editTableOfContents.importBookmarksFromClipboard=Paste from clipboard -editTableOfContents.exportBookmarksDefault=Export -editTableOfContents.exportBookmarksAsJson=Download as JSON -editTableOfContents.exportBookmarksAsText=Copy as text -editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document. -editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks. -editTableOfContents.desc.3=Each bookmark requires a title and target page number. -editTableOfContents.submit=Apply Table of Contents +editTableOfContents.tags=oznake,sadržaj,navigacija,indeks,sadržaj,poglavlja,odjeljci,obris +editTableOfContents.title=Uredi sadržaj +editTableOfContents.header=Dodajte ili uredite PDF sadržaj +editTableOfContents.replaceExisting=Zamijeni postojeće oznake (poništi odabir za dodavanje postojećim) +editTableOfContents.editorTitle=Uređivač oznaka +editTableOfContents.editorDesc=Dodajte i rasporedite oznake ispod. Kliknite + za dodavanje podređenih oznaka. +editTableOfContents.addBookmark=Dodaj novu oznaku +editTableOfContents.importBookmarksDefault=Uvezi +editTableOfContents.importBookmarksFromJsonFile=Učitaj JSON datoteku +editTableOfContents.importBookmarksFromClipboard=Zalijepi iz međuspremnika +editTableOfContents.exportBookmarksDefault=Izvezi +editTableOfContents.exportBookmarksAsJson=Preuzmi kao JSON +editTableOfContents.exportBookmarksAsText=Kopiraj kao tekst +editTableOfContents.desc.1=Ovaj alat omogućuje dodavanje ili uređivanje sadržaja (oznaka) u PDF dokumentu. +editTableOfContents.desc.2=Možete stvoriti hijerarhijsku strukturu dodavanjem podređenih oznaka roditeljskim oznakama. +editTableOfContents.desc.3=Svaka oznaka zahtijeva naslov i ciljni broj stranice. +editTableOfContents.submit=Primijeni sadržaj diff --git a/app/core/src/main/resources/messages_hu_HU.properties b/app/core/src/main/resources/messages_hu_HU.properties index 7845c3fce..ed86fa736 100644 --- a/app/core/src/main/resources/messages_hu_HU.properties +++ b/app/core/src/main/resources/messages_hu_HU.properties @@ -137,6 +137,7 @@ lang.yor=joruba addPageNumbers.fontSize=Betűméret addPageNumbers.fontName=Betűtípus +addPageNumbers.fontColor=Betűszín pdfPrompt=PDF-fájl kiválasztása multiPdfPrompt=PDF-fájlok kiválasztása (2+) multiPdfDropPrompt=Válassza ki (vagy húzza ide) az összes szükséges PDF-fájlt @@ -193,6 +194,7 @@ error.fileFormatRequired=A fájlnak {0} formátumúnak kell lennie error.invalidFormat=Érvénytelen {0} formátum: {1} error.endpointDisabled=Ezt a végpontot a rendszergazda letiltotta error.urlNotReachable=Az URL nem érhető el, kérjük, adjon meg érvényes URL-t +error.invalidUrlFormat=Érvénytelen URL. A megadott formátum érvénytelen. # DPI and image rendering messages - used by frontend for dynamic translation # Backend sends: [TRANSLATE:messageKey:arg1,arg2] English message @@ -482,7 +484,7 @@ adminUserSettings.teamName=Csapat neve adminUserSettings.teamExists=A csapat már létezik adminUserSettings.teamCreated=Csapat sikeresen létrehozva adminUserSettings.teamChanged=A felhasználó csapata frissítve lett -adminUserSettings.teamHidden=Hidden +adminUserSettings.teamHidden=Rejtett csapat adminUserSettings.totalMembers=Összes tag adminUserSettings.confirmDeleteTeam=Biztosan törli ezt a csapatot? @@ -502,7 +504,7 @@ team.confirm.moveUser=Biztosan át akarja helyezni ezt a felhasználót a(z) "{0 team.userAdded=Felhasználó sikeresen hozzáadva a csapathoz team.back=Vissza a csapatokhoz team.internal=Belső csapat -team.internalTeamNotAccessible=A belső csapat egy rendszer csapat, és nem érhető el +team.internalTeamNotAccessible=A belső csapat rendszerszintű, ezért nem érhető el. team.cannotMoveInternalUsers=A belső csapatban lévő felhasználók nem mozgathatók más csapatokba. team.hidden=Rejtett csapat team.name=Csapat neve @@ -604,6 +606,22 @@ home.imageToPdf.title=Kép PDF-be home.imageToPdf.desc=Kép (PNG, JPEG, GIF) konvertálása PDF-fé. imageToPdf.tags=konverzió,kép,jpg,fotó,fénykép +home.cbzToPdf.title=CBZ konvertálása PDF-be +home.cbzToPdf.desc=CBZ képregény archívumok konvertálása PDF formátumba. +cbzToPdf.tags=konverzió,képregény,könyv,archívum,cbz,zip + +home.cbrToPdf.title=CBR konvertálása PDF-be +home.cbrToPdf.desc=CBR képregény archívumok konvertálása PDF formátumba. +cbrToPdf.tags=konverzió,képregény,könyv,archívum,cbr,rar + +home.pdfToCbz.title=PDF konvertálása CBZ-be +home.pdfToCbz.desc=PDF fájlok konvertálása CBZ képregény archívumokba. +pdfToCbz.tags=konverzió,képregény,könyv,archívum,cbz,pdf + +home.pdfToCbr.title=PDF konvertálása CBR-be +home.pdfToCbr.desc=PDF fájlok konvertálása CBR képregény archívumokba. +pdfToCbr.tags=konverzió,képregény,könyv,archívum,cbr,rar + home.pdfToImage.title=PDF képpé home.pdfToImage.desc=PDF konvertálása képpé (PNG, JPEG, GIF). pdfToImage.tags=konverzió,kép,jpg,fotó,fénykép @@ -876,6 +894,12 @@ replace-color.selectText.8=sárga szöveg fekete háttéren replace-color.selectText.9=zöld szöveg fekete háttéren replace-color.selectText.10=Szövegszín kiválasztása replace-color.selectText.11=Háttérszín kiválasztása +replace-color.selectText.12=Színtér konverzió (CMYK nyomtatáshoz) +replace-color.selectText.13=CMYK színtér konverzió +replace-color.selectText.14=Ez az opció a PDF-et RGB színtérből CMYK színtérbe konvertálja, amely professzionális nyomtatásra optimalizált. Ez a folyamat: +replace-color.selectText.15=A színeket CMYK (Cián, Magenta, Sárga, Fekete) színmodellre konvertálja, amit a professzionális nyomtatók használnak +replace-color.selectText.16=A PDF-et nyomdai előkészítési beállításokkal optimalizálja nyomtatásra +replace-color.selectText.17=Enyhe színváltozásokat okozhat, mivel a CMYK kisebb színskálával rendelkezik, mint az RGB replace-color.submit=Csere @@ -908,7 +932,7 @@ login.alreadyLoggedIn=Már be van jelentkezve login.alreadyLoggedIn2=eszközön. Kérjük, jelentkezzen ki az eszközökről és próbálja újra. login.toManySessions=Túl sok aktív munkamenet login.logoutMessage=Sikeresen kijelentkezett. -login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator. +login.invalidInResponseTo=A kért SAML válasz érvénytelen vagy lejárt. Kérjük, forduljon a rendszergazdához. #auto-redact autoRedact.title=Automatikus kitakarás @@ -1217,6 +1241,7 @@ sign.previous=Előző oldal sign.maintainRatio=Képarány fenntartása váltása sign.undo=Visszavonás sign.redo=Újra +sign.colour=Aláírás színe #repair repair.title=Javítás @@ -1359,7 +1384,7 @@ multiTool.split=Felosztás multiTool.moveLeft=Mozgatás balra multiTool.moveRight=Mozgatás jobbra multiTool.delete=Törlés -multiTool.dragDropMessage=Oldal(ak) kiválasztva +multiTool.dragDropMessage=Oldalak kiválasztva multiTool.undo=Visszavonás multiTool.redo=Újra @@ -1423,6 +1448,34 @@ imageToPDF.selectText.3=Több fájl kezelése (csak több kép esetén engedély imageToPDF.selectText.4=Egyesítés egy PDF-be imageToPDF.selectText.5=Konvertálás külön PDF-ekbe +#cbzToPDF +cbzToPDF.title=CBZ konvertálása PDF-be +cbzToPDF.header=CBZ konvertálása PDF-be +cbzToPDF.submit=Konvertálás PDF-be +cbzToPDF.selectText=Válassza ki a CBZ fájlt +cbzToPDF.optimizeForEbook=PDF optimalizálása e-könyv olvasókhoz (Ghostscript használatával) + +#pdfToCBZ +pdfToCBZ.title=PDF konvertálása CBZ-be +pdfToCBZ.header=PDF konvertálása CBZ-be +pdfToCBZ.submit=Konvertálás CBZ-be +pdfToCBZ.selectText=PDF fájl kiválasztása +pdfToCBZ.dpi=DPI (Dots Per Inch) + +#cbrToPDF +cbrToPDF.title=CBR konvertálása PDF-be +cbrToPDF.header=CBR konvertálása PDF-be +cbrToPDF.submit=Konvertálás PDF-be +cbrToPDF.selectText=CBR fájl kiválasztása +cbrToPDF.optimizeForEbook=PDF optimalizálása e-könyv olvasókhoz (Ghostscript használatával) + +#pdfToCBR +pdfToCBR.title=PDF konvertálása CBR-be +pdfToCBR.header=PDF konvertálása CBR-be +pdfToCBR.submit=Konvertálás CBR-be +pdfToCBR.selectText=PDF fájl kiválasztása +pdfToCBR.dpi=DPI (Dots Per Inch) +pdfToCBR.dpiHelp=Nagyobb DPI jobb minőséget, de nagyobb fájlméretet eredményez. #pdfToImage pdfToImage.title=PDF képpé alakítása @@ -1435,10 +1488,11 @@ pdfToImage.colorType=Színtípus pdfToImage.color=Színes pdfToImage.grey=Szürkeárnyalatos pdfToImage.blackwhite=Fekete-fehér (adatvesztéssel járhat!) -pdfToImage.dpi=DPI (The server limit is {0} dpi) +pdfToImage.dpi=DPI (a szerver felső határa: {0} dpi) pdfToImage.submit=Konvertálás pdfToImage.info=Python nincs telepítve. WebP konverzióhoz szükséges. pdfToImage.placeholder=(pl. 1,2,8 vagy 4,7,12-16 vagy 2n-1) +pdfToImage.includeAnnotations=Megjegyzések belefoglalása (kommentek, kiemelések stb.) #addPassword @@ -1893,12 +1947,12 @@ editTableOfContents.replaceExisting=Meglévő könyvjelzők cseréje (törölje editTableOfContents.editorTitle=Könyvjelző szerkesztő editTableOfContents.editorDesc=Könyvjelzők hozzáadása és rendezése lent. Kattintson a + gombra gyermek könyvjelzők hozzáadásához. editTableOfContents.addBookmark=Új könyvjelző hozzáadása -editTableOfContents.importBookmarksDefault=Import -editTableOfContents.importBookmarksFromJsonFile=Upload JSON file -editTableOfContents.importBookmarksFromClipboard=Paste from clipboard -editTableOfContents.exportBookmarksDefault=Export -editTableOfContents.exportBookmarksAsJson=Download as JSON -editTableOfContents.exportBookmarksAsText=Copy as text +editTableOfContents.importBookmarksDefault=Importálás +editTableOfContents.importBookmarksFromJsonFile=JSON fájl feltöltése +editTableOfContents.importBookmarksFromClipboard=Beillesztés vágólapról +editTableOfContents.exportBookmarksDefault=Exportálás +editTableOfContents.exportBookmarksAsJson=Letöltés JSON formátumban +editTableOfContents.exportBookmarksAsText=Másolás szövegként editTableOfContents.desc.1=Ez az eszköz lehetővé teszi a tartalomjegyzék (könyvjelzők) hozzáadását vagy szerkesztését egy PDF dokumentumban. editTableOfContents.desc.2=Hierarchikus struktúrákat hozhat létre, ha gyermek könyvjelzőket ad a szülő könyvjelzőkhöz. editTableOfContents.desc.3=Minden könyvjelzőhöz szükséges egy cím és egy céloldalszám. diff --git a/app/core/src/main/resources/messages_id_ID.properties b/app/core/src/main/resources/messages_id_ID.properties index d06da87ab..2af179f5d 100644 --- a/app/core/src/main/resources/messages_id_ID.properties +++ b/app/core/src/main/resources/messages_id_ID.properties @@ -137,6 +137,7 @@ lang.yor=Yoruba addPageNumbers.fontSize=Ukuran Fonta addPageNumbers.fontName=Nama Fonta +addPageNumbers.fontColor=Font Colour pdfPrompt=Pilih PDF multiPdfPrompt=Pilih PDF (2+) multiPdfDropPrompt=Pilih (atau seret & letakkan)) semua PDF yang Anda butuhkan @@ -193,6 +194,7 @@ error.fileFormatRequired=File must be in {0} format error.invalidFormat=Invalid {0} format: {1} error.endpointDisabled=This endpoint has been disabled by the admin error.urlNotReachable=URL is not reachable, please provide a valid URL +error.invalidUrlFormat=Invalid URL format provided. The provided format is invalid. # DPI and image rendering messages - used by frontend for dynamic translation # Backend sends: [TRANSLATE:messageKey:arg1,arg2] English message @@ -604,6 +606,22 @@ home.imageToPdf.title=Gambar ke PDF home.imageToPdf.desc=Mengonversi gambar (PNG, JPEG, GIF) ke PDF. imageToPdf.tags=konversi,img,jpg,gambar,foto +home.cbzToPdf.title=CBZ to PDF +home.cbzToPdf.desc=Convert CBZ comic book archives to PDF format. +cbzToPdf.tags=conversion,comic,book,archive,cbz,zip + +home.cbrToPdf.title=CBR to PDF +home.cbrToPdf.desc=Convert CBR comic book archives to PDF format. +cbrToPdf.tags=conversion,comic,book,archive,cbr,rar + +home.pdfToCbz.title=PDF to CBZ +home.pdfToCbz.desc=Convert PDF files to CBZ comic book archives. +pdfToCbz.tags=conversion,comic,book,archive,cbz,pdf + +home.pdfToCbr.title=PDF to CBR +home.pdfToCbr.desc=Convert PDF files to CBR comic book archives. +pdfToCbr.tags=conversion,comic,book,archive,cbr,rar + home.pdfToImage.title=PDF ke Gambar home.pdfToImage.desc=Mengonversi PDF ke gambar. (PNG, JPEG, GIF) pdfToImage.tags=konversi,img,jpg,gambar,foto @@ -876,6 +894,12 @@ replace-color.selectText.8=teks kuning di latar belakang hitam replace-color.selectText.9=teks hijau di latar belakang hitam replace-color.selectText.10=Pilih warna teks replace-color.selectText.11=Pilih warna latar belakang +replace-color.selectText.12=Colour Space Conversion (CMYK for Printing) +replace-color.selectText.13=CMYK Colour Space Conversion +replace-color.selectText.14=This option converts the PDF from RGB colour space to CMYK colour space, which is optimized for professional printing. This process: +replace-color.selectText.15=Converts colours to CMYK (Cyan, Magenta, Yellow, Black) colour model used by professional printers +replace-color.selectText.16=Optimizes the PDF for print production with prepress settings +replace-color.selectText.17=May result in slight colour changes as CMYK has a smaller colour gamut than RGB replace-color.submit=Ganti @@ -1081,7 +1105,7 @@ addPageNumbers.selectText.5=Halaman ke Nomor addPageNumbers.selectText.6=Teks Khusus addPageNumbers.customTextDesc=Teks Khusus addPageNumbers.numberPagesDesc=Halaman mana yang akan diberi nomor, default 'semua', juga menerima 1-5 atau 2,5,9, dll. -addPageNumbers.customNumberDesc=Default untuk {n}, juga menerima 'Halaman {n} dari {total}', 'Teks-{n}', '{nama berkas}-{n}' +addPageNumbers.customNumberDesc=Default untuk {n}, juga menerima 'Halaman {n} dari {total}', 'Teks-{n}', '{filename}-{n}' addPageNumbers.submit=Tambahkan Nomor Halaman @@ -1217,6 +1241,7 @@ sign.previous=Previous page sign.maintainRatio=Toggle maintain aspect ratio sign.undo=Undo sign.redo=Redo +sign.colour=Signature Colour #repair repair.title=Perbaiki @@ -1423,6 +1448,34 @@ imageToPDF.selectText.3=Logika multi berkas (Hanya diaktifkan jika bekerja denga imageToPDF.selectText.4=Gabungkan menjadi satu PDF imageToPDF.selectText.5=Mengonversi ke PDF yang terpisah +#cbzToPDF +cbzToPDF.title=CBZ to PDF +cbzToPDF.header=CBZ to PDF +cbzToPDF.submit=Convert to PDF +cbzToPDF.selectText=Select CBZ file +cbzToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBZ +pdfToCBZ.title=PDF to CBZ +pdfToCBZ.header=PDF to CBZ +pdfToCBZ.submit=Convert to CBZ +pdfToCBZ.selectText=Select PDF file +pdfToCBZ.dpi=DPI (Dots Per Inch) + +#cbrToPDF +cbrToPDF.title=CBR to PDF +cbrToPDF.header=CBR to PDF +cbrToPDF.submit=Convert to PDF +cbrToPDF.selectText=Select CBR file +cbrToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBR +pdfToCBR.title=PDF to CBR +pdfToCBR.header=PDF to CBR +pdfToCBR.submit=Convert to CBR +pdfToCBR.selectText=Select PDF file +pdfToCBR.dpi=DPI (Dots Per Inch) +pdfToCBR.dpiHelp=Higher DPI results in better quality but larger file size. #pdfToImage pdfToImage.title=PDF ke Gambar @@ -1439,6 +1492,7 @@ pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Konversi pdfToImage.info=Python tidak terinstal. Diperlukan untuk konversi WebP. pdfToImage.placeholder=(misalnya 1,2,8 atau 4,7,12-16 atau 2n-1) +pdfToImage.includeAnnotations=Include annotations (comments, highlights etc.) #addPassword diff --git a/app/core/src/main/resources/messages_it_IT.properties b/app/core/src/main/resources/messages_it_IT.properties index 7491624f0..940ba5920 100644 --- a/app/core/src/main/resources/messages_it_IT.properties +++ b/app/core/src/main/resources/messages_it_IT.properties @@ -137,6 +137,7 @@ lang.yor=Yoruba addPageNumbers.fontSize=Dimensione del font addPageNumbers.fontName=Nome del font +addPageNumbers.fontColor=Colore del font pdfPrompt=Scegli PDF multiPdfPrompt=Scegli 2 o più PDF multiPdfDropPrompt=Scegli (o trascina e rilascia) uno o più PDF @@ -193,6 +194,7 @@ error.fileFormatRequired=Il file deve essere nel formato {0} error.invalidFormat=Formato {0} non valido:{1} error.endpointDisabled=Questo endpoint è stato disabilitato dall'amministratore error.urlNotReachable=L'URL non è raggiungibile, inserisci un URL valido +error.invalidUrlFormat=Formato URL non valido. Il formato fornito non è valido. # DPI and image rendering messages - used by frontend for dynamic translation # Backend sends: [TRANSLATE:messageKey:arg1,arg2] English message @@ -604,6 +606,22 @@ home.imageToPdf.title=Da immagine a PDF home.imageToPdf.desc=Converti un'immagine (PNG, JPEG, GIF) in PDF. imageToPdf.tags=conversione,img,jpg,immagine,foto +home.cbzToPdf.title=CBZ to PDF +home.cbzToPdf.desc=Convert CBZ comic book archives to PDF format. +cbzToPdf.tags=conversion,comic,book,archive,cbz,zip + +home.cbrToPdf.title=CBR to PDF +home.cbrToPdf.desc=Convert CBR comic book archives to PDF format. +cbrToPdf.tags=conversion,comic,book,archive,cbr,rar + +home.pdfToCbz.title=PDF to CBZ +home.pdfToCbz.desc=Convert PDF files to CBZ comic book archives. +pdfToCbz.tags=conversion,comic,book,archive,cbz,pdf + +home.pdfToCbr.title=PDF to CBR +home.pdfToCbr.desc=Convert PDF files to CBR comic book archives. +pdfToCbr.tags=conversion,comic,book,archive,cbr,rar + home.pdfToImage.title=Da PDF a immagine home.pdfToImage.desc=Converti un PDF in un'immagine. (PNG, JPEG, GIF) pdfToImage.tags=conversione,img,jpg,immagine,foto @@ -876,6 +894,12 @@ replace-color.selectText.8=Testo giallo su sfondo nero replace-color.selectText.9=Testo verde su sfondo nero replace-color.selectText.10=Scegli il colore del testo replace-color.selectText.11=Scegli il colore di sfondo +replace-color.selectText.12=Conversione dello spazio colore (CMYK per la stampa) +replace-color.selectText.13=Conversione dello spazio colore CMYK +replace-color.selectText.14=Questa opzione converte il PDF dallo spazio colore RGB allo spazio colore CMYK, ottimizzato per la stampa professionale. Questo processo: +replace-color.selectText.15=Converte i colori nel modello di colore CMYK (ciano, magenta, giallo, nero) utilizzato dalle stampanti professionali +replace-color.selectText.16=Ottimizza il PDF per la produzione di stampa con le impostazioni di prestampa +replace-color.selectText.17=Potrebbero verificarsi lievi variazioni di colore poiché CMYK ha una gamma di colori più piccola di RGB replace-color.submit=Sostituisci @@ -908,7 +932,7 @@ login.alreadyLoggedIn=Hai già effettuato l'accesso a login.alreadyLoggedIn2=dispositivi. Esci dai dispositivi e riprova. login.toManySessions=Hai troppe sessioni attive login.logoutMessage=Sei stato disconnesso. -login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator. +login.invalidInResponseTo=La risposta SAML richiesta non è valida o è scaduta. Contattare l'amministratore. #auto-redact autoRedact.title=Redazione automatica @@ -1081,7 +1105,7 @@ addPageNumbers.selectText.5=Pagine da numerare addPageNumbers.selectText.6=Testo personalizzato addPageNumbers.customTextDesc=Testo personalizzato addPageNumbers.numberPagesDesc=Quali pagine numerare, impostazione predefinita "tutte", accetta anche 1-5 o 2,5,9 ecc -addPageNumbers.customNumberDesc=Il valore predefinito è {n}, accetta anche 'Pagina {n} di {total}', 'Testo-{n}', '{filename}-{n} +addPageNumbers.customNumberDesc=Il valore predefinito è {n}, accetta anche 'Pagina {n} di {total}', 'Testo-{n}', '{filename}-{n}' addPageNumbers.submit=Aggiungi numeri di pagina @@ -1217,6 +1241,7 @@ sign.previous=Pagina precedente sign.maintainRatio=Attiva il mantenimento delle proporzioni sign.undo=Annulla sign.redo=Rifare +sign.colour=Colore firma #repair repair.title=Ripara @@ -1423,6 +1448,34 @@ imageToPDF.selectText.3=Logica multi-file (funziona solo se ci sono più immagin imageToPDF.selectText.4=Unisci in un unico PDF imageToPDF.selectText.5=Converti in PDF separati +#cbzToPDF +cbzToPDF.title=CBZ in PDF +cbzToPDF.header=CBZ in PDF +cbzToPDF.submit=Converti in PDF +cbzToPDF.selectText=Seleziona il file CBZ +cbzToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBZ +pdfToCBZ.title=PDF in CBZ +pdfToCBZ.header=PDF in CBZ +pdfToCBZ.submit=Converti in CBZ +pdfToCBZ.selectText=Seleziona file PDF +pdfToCBZ.dpi=DPI (Dots Per Inch) + +#cbrToPDF +cbrToPDF.title=CBR to PDF +cbrToPDF.header=CBR to PDF +cbrToPDF.submit=Convert to PDF +cbrToPDF.selectText=Select CBR file +cbrToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBR +pdfToCBR.title=PDF to CBR +pdfToCBR.header=PDF to CBR +pdfToCBR.submit=Convert to CBR +pdfToCBR.selectText=Select PDF file +pdfToCBR.dpi=DPI (Dots Per Inch) +pdfToCBR.dpiHelp=Higher DPI results in better quality but larger file size. #pdfToImage pdfToImage.title=PDF a immagine @@ -1435,10 +1488,11 @@ pdfToImage.colorType=Tipo di colore pdfToImage.color=A colori pdfToImage.grey=Scala di grigi pdfToImage.blackwhite=Bianco e Nero (potresti perdere dettagli!) -pdfToImage.dpi=DPI (The server limit is {0} dpi) +pdfToImage.dpi=DPI (Il limite del server è {0} dpi) pdfToImage.submit=Converti pdfToImage.info=Python non è installato.È richiesto per la conversione WebP. pdfToImage.placeholder=(es. 1,2,8 o 4,7,12-16 o 2n-1) +pdfToImage.includeAnnotations=Includi annotazioni (commenti, evidenziazioni, ecc.) #addPassword diff --git a/app/core/src/main/resources/messages_ja_JP.properties b/app/core/src/main/resources/messages_ja_JP.properties index f0c987c9d..c497f3450 100644 --- a/app/core/src/main/resources/messages_ja_JP.properties +++ b/app/core/src/main/resources/messages_ja_JP.properties @@ -137,6 +137,7 @@ lang.yor=ヨルバ語 addPageNumbers.fontSize=フォントサイズ addPageNumbers.fontName=フォント名 +addPageNumbers.fontColor=Font Colour pdfPrompt=PDFを選択 multiPdfPrompt=PDFを選択(2つ以上) multiPdfDropPrompt=PDFを選択(又はドラッグ&ドロップ) @@ -193,6 +194,7 @@ error.fileFormatRequired=ファイルは{0}形式である必要があります error.invalidFormat=無効な{0}形式: {1} error.endpointDisabled=このエンドポイントは管理者によって無効になっています error.urlNotReachable=URLにアクセスできません。有効なURLを入力してください +error.invalidUrlFormat=Invalid URL format provided. The provided format is invalid. # DPI and image rendering messages - used by frontend for dynamic translation # Backend sends: [TRANSLATE:messageKey:arg1,arg2] English message @@ -604,6 +606,22 @@ home.imageToPdf.title=画像をPDFに変換 home.imageToPdf.desc=画像 (PNG, JPEG, GIF) をPDFに変換します。 imageToPdf.tags=conversion,img,jpg,picture,photo,psd,photoshop +home.cbzToPdf.title=CBZ to PDF +home.cbzToPdf.desc=Convert CBZ comic book archives to PDF format. +cbzToPdf.tags=conversion,comic,book,archive,cbz,zip + +home.cbrToPdf.title=CBR to PDF +home.cbrToPdf.desc=Convert CBR comic book archives to PDF format. +cbrToPdf.tags=conversion,comic,book,archive,cbr,rar + +home.pdfToCbz.title=PDF to CBZ +home.pdfToCbz.desc=Convert PDF files to CBZ comic book archives. +pdfToCbz.tags=conversion,comic,book,archive,cbz,pdf + +home.pdfToCbr.title=PDF to CBR +home.pdfToCbr.desc=Convert PDF files to CBR comic book archives. +pdfToCbr.tags=conversion,comic,book,archive,cbr,rar + home.pdfToImage.title=PDFを画像に変換 home.pdfToImage.desc=PDFを画像 (PNG, JPEG, GIF) に変換します。 pdfToImage.tags=conversion,img,jpg,picture,photo,psd,photoshop @@ -876,6 +894,12 @@ replace-color.selectText.8=黒背景に黄色文字 replace-color.selectText.9=黒背景に緑文字 replace-color.selectText.10=テキストの色を選択 replace-color.selectText.11=背景色を選択 +replace-color.selectText.12=Colour Space Conversion (CMYK for Printing) +replace-color.selectText.13=CMYK Colour Space Conversion +replace-color.selectText.14=This option converts the PDF from RGB colour space to CMYK colour space, which is optimized for professional printing. This process: +replace-color.selectText.15=Converts colours to CMYK (Cyan, Magenta, Yellow, Black) colour model used by professional printers +replace-color.selectText.16=Optimizes the PDF for print production with prepress settings +replace-color.selectText.17=May result in slight colour changes as CMYK has a smaller colour gamut than RGB replace-color.submit=置換 @@ -1217,6 +1241,7 @@ sign.previous=前のページ sign.maintainRatio=アスペクト比を維持を切替え sign.undo=元に戻す sign.redo=やり直す +sign.colour=Signature Colour #repair repair.title=修復 @@ -1423,6 +1448,34 @@ imageToPDF.selectText.3=マルチファイルの処理(複数の画像を操 imageToPDF.selectText.4=1つのPDFに結合 imageToPDF.selectText.5=個別のPDFに変換 +#cbzToPDF +cbzToPDF.title=CBZ to PDF +cbzToPDF.header=CBZ to PDF +cbzToPDF.submit=Convert to PDF +cbzToPDF.selectText=Select CBZ file +cbzToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBZ +pdfToCBZ.title=PDF to CBZ +pdfToCBZ.header=PDF to CBZ +pdfToCBZ.submit=Convert to CBZ +pdfToCBZ.selectText=Select PDF file +pdfToCBZ.dpi=DPI (Dots Per Inch) + +#cbrToPDF +cbrToPDF.title=CBR to PDF +cbrToPDF.header=CBR to PDF +cbrToPDF.submit=Convert to PDF +cbrToPDF.selectText=Select CBR file +cbrToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBR +pdfToCBR.title=PDF to CBR +pdfToCBR.header=PDF to CBR +pdfToCBR.submit=Convert to CBR +pdfToCBR.selectText=Select PDF file +pdfToCBR.dpi=DPI (Dots Per Inch) +pdfToCBR.dpiHelp=Higher DPI results in better quality but larger file size. #pdfToImage pdfToImage.title=PDFを画像に変換 @@ -1439,6 +1492,7 @@ pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=変換 pdfToImage.info=Pythonがインストールされていません。WebPの変換に必要です。 pdfToImage.placeholder=(例:1,2,8、4,7,12-16、2n-1) +pdfToImage.includeAnnotations=Include annotations (comments, highlights etc.) #addPassword diff --git a/app/core/src/main/resources/messages_ko_KR.properties b/app/core/src/main/resources/messages_ko_KR.properties index 77517a000..7d52924d8 100644 --- a/app/core/src/main/resources/messages_ko_KR.properties +++ b/app/core/src/main/resources/messages_ko_KR.properties @@ -137,6 +137,7 @@ lang.yor=Yoruba addPageNumbers.fontSize=글꼴 크기 addPageNumbers.fontName=글꼴 이름 +addPageNumbers.fontColor=Font Colour pdfPrompt=PDF 선택 multiPdfPrompt=PDF 선택 (2개 이상) multiPdfDropPrompt=필요한 모든 PDF를 선택(또는 끌어다 놓기)하세요 @@ -193,6 +194,7 @@ error.fileFormatRequired=File must be in {0} format error.invalidFormat=Invalid {0} format: {1} error.endpointDisabled=This endpoint has been disabled by the admin error.urlNotReachable=URL is not reachable, please provide a valid URL +error.invalidUrlFormat=Invalid URL format provided. The provided format is invalid. # DPI and image rendering messages - used by frontend for dynamic translation # Backend sends: [TRANSLATE:messageKey:arg1,arg2] English message @@ -604,6 +606,22 @@ home.imageToPdf.title=이미지를 PDF로 home.imageToPdf.desc=이미지(PNG, JPEG, GIF)를 PDF로 변환합니다. imageToPdf.tags=변환,이미지,jpg,사진 +home.cbzToPdf.title=CBZ to PDF +home.cbzToPdf.desc=Convert CBZ comic book archives to PDF format. +cbzToPdf.tags=conversion,comic,book,archive,cbz,zip + +home.cbrToPdf.title=CBR to PDF +home.cbrToPdf.desc=Convert CBR comic book archives to PDF format. +cbrToPdf.tags=conversion,comic,book,archive,cbr,rar + +home.pdfToCbz.title=PDF to CBZ +home.pdfToCbz.desc=Convert PDF files to CBZ comic book archives. +pdfToCbz.tags=conversion,comic,book,archive,cbz,pdf + +home.pdfToCbr.title=PDF to CBR +home.pdfToCbr.desc=Convert PDF files to CBR comic book archives. +pdfToCbr.tags=conversion,comic,book,archive,cbr,rar + home.pdfToImage.title=PDF를 이미지로 home.pdfToImage.desc=PDF를 이미지로 변환합니다. (PNG, JPEG, GIF) pdfToImage.tags=변환,이미지,jpg,사진 @@ -876,6 +894,12 @@ replace-color.selectText.8=검정 배경에 노란색 텍스트 replace-color.selectText.9=검정 배경에 초록색 텍스트 replace-color.selectText.10=텍스트 색상 선택 replace-color.selectText.11=배경 색상 선택 +replace-color.selectText.12=Colour Space Conversion (CMYK for Printing) +replace-color.selectText.13=CMYK Colour Space Conversion +replace-color.selectText.14=This option converts the PDF from RGB colour space to CMYK colour space, which is optimized for professional printing. This process: +replace-color.selectText.15=Converts colours to CMYK (Cyan, Magenta, Yellow, Black) colour model used by professional printers +replace-color.selectText.16=Optimizes the PDF for print production with prepress settings +replace-color.selectText.17=May result in slight colour changes as CMYK has a smaller colour gamut than RGB replace-color.submit=교체 @@ -1217,6 +1241,7 @@ sign.previous=이전 페이지 sign.maintainRatio=종횡비 유지 토글 sign.undo=Undo sign.redo=Redo +sign.colour=Signature Colour #repair repair.title=복구 @@ -1423,6 +1448,34 @@ imageToPDF.selectText.3=다중 파일 로직 (여러 이미지 작업 시에만 imageToPDF.selectText.4=단일 PDF로 병합 imageToPDF.selectText.5=별도의 PDF로 변환 +#cbzToPDF +cbzToPDF.title=CBZ to PDF +cbzToPDF.header=CBZ to PDF +cbzToPDF.submit=Convert to PDF +cbzToPDF.selectText=Select CBZ file +cbzToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBZ +pdfToCBZ.title=PDF to CBZ +pdfToCBZ.header=PDF to CBZ +pdfToCBZ.submit=Convert to CBZ +pdfToCBZ.selectText=Select PDF file +pdfToCBZ.dpi=DPI (Dots Per Inch) + +#cbrToPDF +cbrToPDF.title=CBR to PDF +cbrToPDF.header=CBR to PDF +cbrToPDF.submit=Convert to PDF +cbrToPDF.selectText=Select CBR file +cbrToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBR +pdfToCBR.title=PDF to CBR +pdfToCBR.header=PDF to CBR +pdfToCBR.submit=Convert to CBR +pdfToCBR.selectText=Select PDF file +pdfToCBR.dpi=DPI (Dots Per Inch) +pdfToCBR.dpiHelp=Higher DPI results in better quality but larger file size. #pdfToImage pdfToImage.title=PDF를 이미지로 @@ -1439,6 +1492,7 @@ pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=변환 pdfToImage.info=WebP 변환에는 Python이 필요합니다. Python이 설치되지 않았습니다. pdfToImage.placeholder=(예: 1,2,8 또는 4,7,12-16 또는 2n-1) +pdfToImage.includeAnnotations=Include annotations (comments, highlights etc.) #addPassword diff --git a/app/core/src/main/resources/messages_ml_IN.properties b/app/core/src/main/resources/messages_ml_IN.properties index 356e5f99b..b0549b1ea 100644 --- a/app/core/src/main/resources/messages_ml_IN.properties +++ b/app/core/src/main/resources/messages_ml_IN.properties @@ -137,6 +137,7 @@ lang.yor=Yoruba addPageNumbers.fontSize=അക്ഷര വലുപ്പം addPageNumbers.fontName=അക്ഷരത്തിന്റെ പേര് +addPageNumbers.fontColor=Font Colour pdfPrompt=PDF(കൾ) തിരഞ്ഞെടുക്കുക multiPdfPrompt=PDF-കൾ തിരഞ്ഞെടുക്കുക (2+) multiPdfDropPrompt=നിങ്ങൾക്ക് ആവശ്യമുള്ള എല്ലാ PDF-കളും തിരഞ്ഞെടുക്കുക (അല്ലെങ്കിൽ വലിച്ചിടുക) @@ -193,6 +194,7 @@ error.fileFormatRequired=File must be in {0} format error.invalidFormat=Invalid {0} format: {1} error.endpointDisabled=This endpoint has been disabled by the admin error.urlNotReachable=URL is not reachable, please provide a valid URL +error.invalidUrlFormat=Invalid URL format provided. The provided format is invalid. # DPI and image rendering messages - used by frontend for dynamic translation # Backend sends: [TRANSLATE:messageKey:arg1,arg2] English message @@ -604,6 +606,22 @@ home.imageToPdf.title=ചിത്രം PDF-ലേക്ക് home.imageToPdf.desc=ഒരു ചിത്രം (PNG, JPEG, GIF) PDF-ലേക്ക് മാറ്റുക. imageToPdf.tags=പരിവർത്തനം,img,jpg,ചിത്രം,ഫോട്ടോ +home.cbzToPdf.title=CBZ to PDF +home.cbzToPdf.desc=Convert CBZ comic book archives to PDF format. +cbzToPdf.tags=conversion,comic,book,archive,cbz,zip + +home.cbrToPdf.title=CBR to PDF +home.cbrToPdf.desc=Convert CBR comic book archives to PDF format. +cbrToPdf.tags=conversion,comic,book,archive,cbr,rar + +home.pdfToCbz.title=PDF to CBZ +home.pdfToCbz.desc=Convert PDF files to CBZ comic book archives. +pdfToCbz.tags=conversion,comic,book,archive,cbz,pdf + +home.pdfToCbr.title=PDF to CBR +home.pdfToCbr.desc=Convert PDF files to CBR comic book archives. +pdfToCbr.tags=conversion,comic,book,archive,cbr,rar + home.pdfToImage.title=PDF ചിത്രത്തിലേക്ക് home.pdfToImage.desc=ഒരു PDF ചിത്രത്തിലേക്ക് മാറ്റുക. (PNG, JPEG, GIF) pdfToImage.tags=പരിവർത്തനം,img,jpg,ചിത്രം,ഫോട്ടോ @@ -876,6 +894,12 @@ replace-color.selectText.8=കറുത്ത പശ്ചാത്തലത് replace-color.selectText.9=കറുത്ത പശ്ചാത്തലത്തിൽ പച്ച ടെക്സ്റ്റ് replace-color.selectText.10=ടെക്സ്റ്റ് നിറം തിരഞ്ഞെടുക്കുക replace-color.selectText.11=പശ്ചാത്തല നിറം തിരഞ്ഞെടുക്കുക +replace-color.selectText.12=Colour Space Conversion (CMYK for Printing) +replace-color.selectText.13=CMYK Colour Space Conversion +replace-color.selectText.14=This option converts the PDF from RGB colour space to CMYK colour space, which is optimized for professional printing. This process: +replace-color.selectText.15=Converts colours to CMYK (Cyan, Magenta, Yellow, Black) colour model used by professional printers +replace-color.selectText.16=Optimizes the PDF for print production with prepress settings +replace-color.selectText.17=May result in slight colour changes as CMYK has a smaller colour gamut than RGB replace-color.submit=മാറ്റുക @@ -1217,6 +1241,7 @@ sign.previous=മുമ്പത്തെ പേജ് sign.maintainRatio=വീക്ഷണാനുപാതം നിലനിർത്തുക ടോഗിൾ ചെയ്യുക sign.undo=പഴയപടി ആക്കുക sign.redo=വീണ്ടും ചെയ്യുക +sign.colour=Signature Colour #repair repair.title=നന്നാക്കുക @@ -1423,6 +1448,34 @@ imageToPDF.selectText.3=മൾട്ടി ഫയൽ ലോജിക് (ഒന imageToPDF.selectText.4=ഒരൊറ്റ PDF-ലേക്ക് ലയിപ്പിക്കുക imageToPDF.selectText.5=വേറിട്ട PDF-കളിലേക്ക് മാറ്റുക +#cbzToPDF +cbzToPDF.title=CBZ to PDF +cbzToPDF.header=CBZ to PDF +cbzToPDF.submit=Convert to PDF +cbzToPDF.selectText=Select CBZ file +cbzToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBZ +pdfToCBZ.title=PDF to CBZ +pdfToCBZ.header=PDF to CBZ +pdfToCBZ.submit=Convert to CBZ +pdfToCBZ.selectText=Select PDF file +pdfToCBZ.dpi=DPI (Dots Per Inch) + +#cbrToPDF +cbrToPDF.title=CBR to PDF +cbrToPDF.header=CBR to PDF +cbrToPDF.submit=Convert to PDF +cbrToPDF.selectText=Select CBR file +cbrToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBR +pdfToCBR.title=PDF to CBR +pdfToCBR.header=PDF to CBR +pdfToCBR.submit=Convert to CBR +pdfToCBR.selectText=Select PDF file +pdfToCBR.dpi=DPI (Dots Per Inch) +pdfToCBR.dpiHelp=Higher DPI results in better quality but larger file size. #pdfToImage pdfToImage.title=PDF ചിത്രത്തിലേക്ക് @@ -1439,6 +1492,7 @@ pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=പരിവർത്തനം ചെയ്യുക pdfToImage.info=പൈത്തൺ ഇൻസ്റ്റാൾ ചെയ്തിട്ടില്ല. WebP പരിവർത്തനത്തിന് ആവശ്യമാണ്. pdfToImage.placeholder=(ഉദാ. 1,2,8 അല്ലെങ്കിൽ 4,7,12-16 അല്ലെങ്കിൽ 2n-1) +pdfToImage.includeAnnotations=Include annotations (comments, highlights etc.) #addPassword diff --git a/app/core/src/main/resources/messages_nl_NL.properties b/app/core/src/main/resources/messages_nl_NL.properties index f7aa1e805..e46e36bbb 100644 --- a/app/core/src/main/resources/messages_nl_NL.properties +++ b/app/core/src/main/resources/messages_nl_NL.properties @@ -137,6 +137,7 @@ lang.yor=Yoruba addPageNumbers.fontSize=Lettertypegrootte addPageNumbers.fontName=Lettertypenaam +addPageNumbers.fontColor=Font Colour pdfPrompt=Selecteer PDF('s) multiPdfPrompt=Selecteer PDF's (2+) multiPdfDropPrompt=Selecteer (of sleep & zet neer) alle PDF's die je nodig hebt @@ -193,6 +194,7 @@ error.fileFormatRequired=File must be in {0} format error.invalidFormat=Invalid {0} format: {1} error.endpointDisabled=This endpoint has been disabled by the admin error.urlNotReachable=URL is not reachable, please provide a valid URL +error.invalidUrlFormat=Invalid URL format provided. The provided format is invalid. # DPI and image rendering messages - used by frontend for dynamic translation # Backend sends: [TRANSLATE:messageKey:arg1,arg2] English message @@ -604,6 +606,22 @@ home.imageToPdf.title=Afbeelding naar PDF home.imageToPdf.desc=Converteer een afbeelding (PNG, JPEG, GIF) naar PDF. imageToPdf.tags=conversie,img,jpg,foto +home.cbzToPdf.title=CBZ to PDF +home.cbzToPdf.desc=Convert CBZ comic book archives to PDF format. +cbzToPdf.tags=conversion,comic,book,archive,cbz,zip + +home.cbrToPdf.title=CBR to PDF +home.cbrToPdf.desc=Convert CBR comic book archives to PDF format. +cbrToPdf.tags=conversion,comic,book,archive,cbr,rar + +home.pdfToCbz.title=PDF to CBZ +home.pdfToCbz.desc=Convert PDF files to CBZ comic book archives. +pdfToCbz.tags=conversion,comic,book,archive,cbz,pdf + +home.pdfToCbr.title=PDF to CBR +home.pdfToCbr.desc=Convert PDF files to CBR comic book archives. +pdfToCbr.tags=conversion,comic,book,archive,cbr,rar + home.pdfToImage.title=PDF naar Afbeelding home.pdfToImage.desc=Converteer een PDF naar een afbeelding. (PNG, JPEG, GIF) pdfToImage.tags=conversie,img,jpg,foto @@ -876,6 +894,12 @@ replace-color.selectText.8=gele tekst op een zwart grondvlak replace-color.selectText.9=groene tekst op een zwart grondvlak replace-color.selectText.10=Kies de tekstkleur replace-color.selectText.11=Kies het achtergrondkleur +replace-color.selectText.12=Colour Space Conversion (CMYK for Printing) +replace-color.selectText.13=CMYK Colour Space Conversion +replace-color.selectText.14=This option converts the PDF from RGB colour space to CMYK colour space, which is optimized for professional printing. This process: +replace-color.selectText.15=Converts colours to CMYK (Cyan, Magenta, Yellow, Black) colour model used by professional printers +replace-color.selectText.16=Optimizes the PDF for print production with prepress settings +replace-color.selectText.17=May result in slight colour changes as CMYK has a smaller colour gamut than RGB replace-color.submit=Vervang @@ -1081,7 +1105,7 @@ addPageNumbers.selectText.5=Pagina's om te nummeren addPageNumbers.selectText.6=Aangepaste tekst addPageNumbers.customTextDesc=Aangepaste tekst addPageNumbers.numberPagesDesc=Welke pagina's genummerd moeten worden, standaard 'all', accepteert ook 1-5 of 2,5,9 etc -addPageNumbers.customNumberDesc=Standaard {n}, accepteert ook 'Pagina {n} van {total}', 'Tekst-{n}', '{filename}-{n} +addPageNumbers.customNumberDesc=Standaard {n}, accepteert ook 'Pagina {n} van {total}', 'Tekst-{n}', '{filename}-{n}' addPageNumbers.submit=Paginanummers toevoegen @@ -1217,6 +1241,7 @@ sign.previous=Previous page sign.maintainRatio=Toggle maintain aspect ratio sign.undo=Undo sign.redo=Redo +sign.colour=Signature Colour #repair repair.title=Repareren @@ -1423,6 +1448,34 @@ imageToPDF.selectText.3=Meervoudige bestandslogica (Alleen ingeschakeld bij werk imageToPDF.selectText.4=Voeg samen in één PDF imageToPDF.selectText.5=Zet om naar afzonderlijke PDF's +#cbzToPDF +cbzToPDF.title=CBZ to PDF +cbzToPDF.header=CBZ to PDF +cbzToPDF.submit=Convert to PDF +cbzToPDF.selectText=Select CBZ file +cbzToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBZ +pdfToCBZ.title=PDF to CBZ +pdfToCBZ.header=PDF to CBZ +pdfToCBZ.submit=Convert to CBZ +pdfToCBZ.selectText=Select PDF file +pdfToCBZ.dpi=DPI (Dots Per Inch) + +#cbrToPDF +cbrToPDF.title=CBR to PDF +cbrToPDF.header=CBR to PDF +cbrToPDF.submit=Convert to PDF +cbrToPDF.selectText=Select CBR file +cbrToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBR +pdfToCBR.title=PDF to CBR +pdfToCBR.header=PDF to CBR +pdfToCBR.submit=Convert to CBR +pdfToCBR.selectText=Select PDF file +pdfToCBR.dpi=DPI (Dots Per Inch) +pdfToCBR.dpiHelp=Higher DPI results in better quality but larger file size. #pdfToImage pdfToImage.title=PDF naar afbeelding @@ -1439,6 +1492,7 @@ pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Omzetten pdfToImage.info=Python is niet geïnstalleerd. Vereist voor WebP-conversie. pdfToImage.placeholder=(bijv. 1,2,8 of 4,7,12-16 of 2n-1) +pdfToImage.includeAnnotations=Include annotations (comments, highlights etc.) #addPassword diff --git a/app/core/src/main/resources/messages_no_NB.properties b/app/core/src/main/resources/messages_no_NB.properties index ae9091cf5..4bf86e2b9 100644 --- a/app/core/src/main/resources/messages_no_NB.properties +++ b/app/core/src/main/resources/messages_no_NB.properties @@ -137,6 +137,7 @@ lang.yor=Yoruba addPageNumbers.fontSize=Skriftstørrelse addPageNumbers.fontName=Skrifttype +addPageNumbers.fontColor=Font Colour pdfPrompt=Velg PDF(er) multiPdfPrompt=Velg PDF-filer (2+) multiPdfDropPrompt=Velg (eller dra og slipp) alle PDF-ene du trenger @@ -193,6 +194,7 @@ error.fileFormatRequired=File must be in {0} format error.invalidFormat=Invalid {0} format: {1} error.endpointDisabled=This endpoint has been disabled by the admin error.urlNotReachable=URL is not reachable, please provide a valid URL +error.invalidUrlFormat=Invalid URL format provided. The provided format is invalid. # DPI and image rendering messages - used by frontend for dynamic translation # Backend sends: [TRANSLATE:messageKey:arg1,arg2] English message @@ -604,6 +606,22 @@ home.imageToPdf.title=Bilde til PDF home.imageToPdf.desc=Konverter et bilde (PNG, JPEG, GIF) til PDF. imageToPdf.tags=konvertering,bilde,jpg,foto +home.cbzToPdf.title=CBZ to PDF +home.cbzToPdf.desc=Convert CBZ comic book archives to PDF format. +cbzToPdf.tags=conversion,comic,book,archive,cbz,zip + +home.cbrToPdf.title=CBR to PDF +home.cbrToPdf.desc=Convert CBR comic book archives to PDF format. +cbrToPdf.tags=conversion,comic,book,archive,cbr,rar + +home.pdfToCbz.title=PDF to CBZ +home.pdfToCbz.desc=Convert PDF files to CBZ comic book archives. +pdfToCbz.tags=conversion,comic,book,archive,cbz,pdf + +home.pdfToCbr.title=PDF to CBR +home.pdfToCbr.desc=Convert PDF files to CBR comic book archives. +pdfToCbr.tags=conversion,comic,book,archive,cbr,rar + home.pdfToImage.title=PDF til Bilde home.pdfToImage.desc=Konverter en PDF til et bilde. (PNG, JPEG, GIF) pdfToImage.tags=konvertering,bilde,jpg,foto @@ -876,6 +894,12 @@ replace-color.selectText.8=Gul tekst på svart bakgrunn replace-color.selectText.9=Grønn tekst på svart bakgrunn replace-color.selectText.10=Velg tekstfarge replace-color.selectText.11=Velg bakgrunnsfarge +replace-color.selectText.12=Colour Space Conversion (CMYK for Printing) +replace-color.selectText.13=CMYK Colour Space Conversion +replace-color.selectText.14=This option converts the PDF from RGB colour space to CMYK colour space, which is optimized for professional printing. This process: +replace-color.selectText.15=Converts colours to CMYK (Cyan, Magenta, Yellow, Black) colour model used by professional printers +replace-color.selectText.16=Optimizes the PDF for print production with prepress settings +replace-color.selectText.17=May result in slight colour changes as CMYK has a smaller colour gamut than RGB replace-color.submit=Erstatt @@ -1081,7 +1105,7 @@ addPageNumbers.selectText.5=Sider å nummerere addPageNumbers.selectText.6=Tilpasset Tekst addPageNumbers.customTextDesc=Tilpasset Tekst addPageNumbers.numberPagesDesc=Hvilke sider som skal nummereres, standard 'alle', aksepterer også 1-5 eller 2,5,9 osv. -addPageNumbers.customNumberDesc=Standard til {n}, aksepterer også 'Side {n} av {total}', 'Tekst-{n}', '{filnavn}-{n} +addPageNumbers.customNumberDesc=Standard til {n}, aksepterer også 'Side {n} av {total}', 'Tekst-{n}', '{filename}-{n}' addPageNumbers.submit=Legg til Sidetall @@ -1217,6 +1241,7 @@ sign.previous=Forrige side sign.maintainRatio=Bytt behold sideforhold sign.undo=Angre sign.redo=Gjør om +sign.colour=Signature Colour #repair repair.title=Reparer @@ -1423,6 +1448,34 @@ imageToPDF.selectText.3=Flere fillogikk (Bare aktivert ved arbeid med flere bild imageToPDF.selectText.4=Slå sammen til en enkelt PDF imageToPDF.selectText.5=Konverter til separate PDF-filer +#cbzToPDF +cbzToPDF.title=CBZ to PDF +cbzToPDF.header=CBZ to PDF +cbzToPDF.submit=Convert to PDF +cbzToPDF.selectText=Select CBZ file +cbzToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBZ +pdfToCBZ.title=PDF to CBZ +pdfToCBZ.header=PDF to CBZ +pdfToCBZ.submit=Convert to CBZ +pdfToCBZ.selectText=Select PDF file +pdfToCBZ.dpi=DPI (Dots Per Inch) + +#cbrToPDF +cbrToPDF.title=CBR to PDF +cbrToPDF.header=CBR to PDF +cbrToPDF.submit=Convert to PDF +cbrToPDF.selectText=Select CBR file +cbrToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBR +pdfToCBR.title=PDF to CBR +pdfToCBR.header=PDF to CBR +pdfToCBR.submit=Convert to CBR +pdfToCBR.selectText=Select PDF file +pdfToCBR.dpi=DPI (Dots Per Inch) +pdfToCBR.dpiHelp=Higher DPI results in better quality but larger file size. #pdfToImage pdfToImage.title=PDF til bilde @@ -1439,6 +1492,7 @@ pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Konverter pdfToImage.info=Python is not installed. Required for WebP conversion. pdfToImage.placeholder=(f.eks. 1,2,8 eller 4,7,12-16 eller 2n-1) +pdfToImage.includeAnnotations=Include annotations (comments, highlights etc.) #addPassword diff --git a/app/core/src/main/resources/messages_pl_PL.properties b/app/core/src/main/resources/messages_pl_PL.properties index 9c5dc670e..d3389e68b 100644 --- a/app/core/src/main/resources/messages_pl_PL.properties +++ b/app/core/src/main/resources/messages_pl_PL.properties @@ -137,6 +137,7 @@ lang.yor=Yoruba addPageNumbers.fontSize=Rozmiar Czcionki addPageNumbers.fontName=Nazwa Czcionki +addPageNumbers.fontColor=Font Colour pdfPrompt=Wybierz PDF multiPdfPrompt=Wybierz PDF (2+) multiPdfDropPrompt=Wybierz (lub przeciągnij i puść) wszystkie dokumenty PDF @@ -193,6 +194,7 @@ error.fileFormatRequired=File must be in {0} format error.invalidFormat=Invalid {0} format: {1} error.endpointDisabled=This endpoint has been disabled by the admin error.urlNotReachable=URL is not reachable, please provide a valid URL +error.invalidUrlFormat=Invalid URL format provided. The provided format is invalid. # DPI and image rendering messages - used by frontend for dynamic translation # Backend sends: [TRANSLATE:messageKey:arg1,arg2] English message @@ -604,6 +606,22 @@ home.imageToPdf.title=Obraz na PDF home.imageToPdf.desc=Konwertuj obraz (PNG, JPEG, GIF) do dokumentu PDF. imageToPdf.tags=konwersja,img,jpg,obraz,zdjęcie +home.cbzToPdf.title=CBZ to PDF +home.cbzToPdf.desc=Convert CBZ comic book archives to PDF format. +cbzToPdf.tags=conversion,comic,book,archive,cbz,zip + +home.cbrToPdf.title=CBR to PDF +home.cbrToPdf.desc=Convert CBR comic book archives to PDF format. +cbrToPdf.tags=conversion,comic,book,archive,cbr,rar + +home.pdfToCbz.title=PDF to CBZ +home.pdfToCbz.desc=Convert PDF files to CBZ comic book archives. +pdfToCbz.tags=conversion,comic,book,archive,cbz,pdf + +home.pdfToCbr.title=PDF to CBR +home.pdfToCbr.desc=Convert PDF files to CBR comic book archives. +pdfToCbr.tags=conversion,comic,book,archive,cbr,rar + home.pdfToImage.title=PDF na Obraz home.pdfToImage.desc=Konwertuj plik PDF na obraz (PNG, JPEG, GIF). pdfToImage.tags=konwersja,img,jpg,obraz,zdjęcie @@ -876,6 +894,12 @@ replace-color.selectText.8=Żółty tekst na czarnym tle replace-color.selectText.9=Zielony tekst na czarnym tle replace-color.selectText.10=Wybierz Kolor tekstu replace-color.selectText.11=Wybierz Kolor tła +replace-color.selectText.12=Colour Space Conversion (CMYK for Printing) +replace-color.selectText.13=CMYK Colour Space Conversion +replace-color.selectText.14=This option converts the PDF from RGB colour space to CMYK colour space, which is optimized for professional printing. This process: +replace-color.selectText.15=Converts colours to CMYK (Cyan, Magenta, Yellow, Black) colour model used by professional printers +replace-color.selectText.16=Optimizes the PDF for print production with prepress settings +replace-color.selectText.17=May result in slight colour changes as CMYK has a smaller colour gamut than RGB replace-color.submit=Zamień @@ -1081,7 +1105,7 @@ addPageNumbers.selectText.5=Ilość stron do ponumerowania addPageNumbers.selectText.6=Tekst własny addPageNumbers.customTextDesc=Tekst własny addPageNumbers.numberPagesDesc=Strony do numeracji, wszystkie (all), 1-5, 2, 5, 9 -addPageNumbers.customNumberDesc=Domyślnie do {n}, również akceptuje 'Strona {n} z {total},Teskt-{n},'{filename}-{n} +addPageNumbers.customNumberDesc=Domyślnie do {n}, również akceptuje 'Strona {n} z {total}', 'Tekst-{n}', '{filename}-{n}' addPageNumbers.submit=Dodaj numerację stron @@ -1217,6 +1241,7 @@ sign.previous=Poprzednia strona sign.maintainRatio=Przełącz zachowanie proporcji sign.undo=Cofnij sign.redo=Ponów +sign.colour=Signature Colour #repair repair.title=Napraw @@ -1423,6 +1448,34 @@ imageToPDF.selectText.3=Logika wielu plików (dostępna tylko w przypadku pracy imageToPDF.selectText.4=Połącz w jeden dokument PDF imageToPDF.selectText.5=Konwertuj na osobne dokumenty PDF +#cbzToPDF +cbzToPDF.title=CBZ to PDF +cbzToPDF.header=CBZ to PDF +cbzToPDF.submit=Convert to PDF +cbzToPDF.selectText=Select CBZ file +cbzToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBZ +pdfToCBZ.title=PDF to CBZ +pdfToCBZ.header=PDF to CBZ +pdfToCBZ.submit=Convert to CBZ +pdfToCBZ.selectText=Select PDF file +pdfToCBZ.dpi=DPI (Dots Per Inch) + +#cbrToPDF +cbrToPDF.title=CBR to PDF +cbrToPDF.header=CBR to PDF +cbrToPDF.submit=Convert to PDF +cbrToPDF.selectText=Select CBR file +cbrToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBR +pdfToCBR.title=PDF to CBR +pdfToCBR.header=PDF to CBR +pdfToCBR.submit=Convert to CBR +pdfToCBR.selectText=Select PDF file +pdfToCBR.dpi=DPI (Dots Per Inch) +pdfToCBR.dpiHelp=Higher DPI results in better quality but larger file size. #pdfToImage pdfToImage.title=PDF na Obraz @@ -1439,6 +1492,7 @@ pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Konwertuj pdfToImage.info=Python nie został zainstalowany. Jest wymagany do konwersji WebP. pdfToImage.placeholder=(przykład 1,2,8 lub 2n-1) +pdfToImage.includeAnnotations=Include annotations (comments, highlights etc.) #addPassword diff --git a/app/core/src/main/resources/messages_pt_BR.properties b/app/core/src/main/resources/messages_pt_BR.properties index bf2cb6a17..3b5baebe0 100644 --- a/app/core/src/main/resources/messages_pt_BR.properties +++ b/app/core/src/main/resources/messages_pt_BR.properties @@ -137,6 +137,7 @@ lang.yor=Yoruba addPageNumbers.fontSize=Tamanho da Fonte addPageNumbers.fontName=Nome da Fonte +addPageNumbers.fontColor=Font Colour pdfPrompt=Selecione o(s) PDF(s) multiPdfPrompt=Selecione os PDFs (2+) multiPdfDropPrompt=Selecione (ou arraste e solte) todos os PDFs desejados: @@ -193,6 +194,7 @@ error.fileFormatRequired=File must be in {0} format error.invalidFormat=Invalid {0} format: {1} error.endpointDisabled=This endpoint has been disabled by the admin error.urlNotReachable=URL is not reachable, please provide a valid URL +error.invalidUrlFormat=Invalid URL format provided. The provided format is invalid. # DPI and image rendering messages - used by frontend for dynamic translation # Backend sends: [TRANSLATE:messageKey:arg1,arg2] English message @@ -604,6 +606,22 @@ home.imageToPdf.title=Imagem para PDF home.imageToPdf.desc=Converter uma imagem (PNG, JPG, GIF) em PDF. imageToPdf.tags=conversão,img,jpg,imagem,foto +home.cbzToPdf.title=CBZ to PDF +home.cbzToPdf.desc=Convert CBZ comic book archives to PDF format. +cbzToPdf.tags=conversion,comic,book,archive,cbz,zip + +home.cbrToPdf.title=CBR to PDF +home.cbrToPdf.desc=Convert CBR comic book archives to PDF format. +cbrToPdf.tags=conversion,comic,book,archive,cbr,rar + +home.pdfToCbz.title=PDF to CBZ +home.pdfToCbz.desc=Convert PDF files to CBZ comic book archives. +pdfToCbz.tags=conversion,comic,book,archive,cbz,pdf + +home.pdfToCbr.title=PDF to CBR +home.pdfToCbr.desc=Convert PDF files to CBR comic book archives. +pdfToCbr.tags=conversion,comic,book,archive,cbr,rar + home.pdfToImage.title=PDF para Imagem home.pdfToImage.desc=Converter PDF em uma imagem (PNG, JPG, GIF e outros). pdfToImage.tags=conversão,img,jpg,imagem,foto @@ -876,6 +894,12 @@ replace-color.selectText.8=Texto amarelo em um plano de fundo preto replace-color.selectText.9=Texto verde em um plano de fundo preto replace-color.selectText.10=Escolha a cor do texto: replace-color.selectText.11=Escolha a cor do plano de fundo: +replace-color.selectText.12=Colour Space Conversion (CMYK for Printing) +replace-color.selectText.13=CMYK Colour Space Conversion +replace-color.selectText.14=This option converts the PDF from RGB colour space to CMYK colour space, which is optimized for professional printing. This process: +replace-color.selectText.15=Converts colours to CMYK (Cyan, Magenta, Yellow, Black) colour model used by professional printers +replace-color.selectText.16=Optimizes the PDF for print production with prepress settings +replace-color.selectText.17=May result in slight colour changes as CMYK has a smaller colour gamut than RGB replace-color.submit=Substituir @@ -1081,7 +1105,7 @@ addPageNumbers.selectText.5=Páginas a numerar: addPageNumbers.selectText.6=Texto personalizado: addPageNumbers.customTextDesc=Texto personalizado: addPageNumbers.numberPagesDesc=Quais páginas numerar, padrão 'todas', também aceita 1-5 ou 2,5,9,etc. -addPageNumbers.customNumberDesc=O padrão é {n}, também aceita 'Página {n} de {total}', 'Texto-{n}', '{nome do arquivo}-{n}' +addPageNumbers.customNumberDesc=O padrão é {n}, também aceita 'Página {n} de {total}', 'Texto-{n}', '{filename}-{n}' addPageNumbers.submit=Adicionar Números de Página @@ -1217,6 +1241,7 @@ sign.previous=Página anterior sign.maintainRatio=Habilitar manter proporção sign.undo=Desfazer sign.redo=Refazer +sign.colour=Signature Colour #repair repair.title=Reparar @@ -1423,6 +1448,34 @@ imageToPDF.selectText.3=Lógica de vários arquivos (Ativada apenas ao trabalhar imageToPDF.selectText.4=Mesclar em um único PDF imageToPDF.selectText.5=Converter em PDFs separados +#cbzToPDF +cbzToPDF.title=CBZ to PDF +cbzToPDF.header=CBZ to PDF +cbzToPDF.submit=Convert to PDF +cbzToPDF.selectText=Select CBZ file +cbzToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBZ +pdfToCBZ.title=PDF to CBZ +pdfToCBZ.header=PDF to CBZ +pdfToCBZ.submit=Convert to CBZ +pdfToCBZ.selectText=Select PDF file +pdfToCBZ.dpi=DPI (Dots Per Inch) + +#cbrToPDF +cbrToPDF.title=CBR to PDF +cbrToPDF.header=CBR to PDF +cbrToPDF.submit=Convert to PDF +cbrToPDF.selectText=Select CBR file +cbrToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBR +pdfToCBR.title=PDF to CBR +pdfToCBR.header=PDF to CBR +pdfToCBR.submit=Convert to CBR +pdfToCBR.selectText=Select PDF file +pdfToCBR.dpi=DPI (Dots Per Inch) +pdfToCBR.dpiHelp=Higher DPI results in better quality but larger file size. #pdfToImage pdfToImage.title=PDF para Imagem @@ -1439,6 +1492,7 @@ pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Converter pdfToImage.info=Python não está instalado. Necessário para conversão WebP. pdfToImage.placeholder=(por exemplo 1,2,8 ou 4,7,12-16 ou 2n-1) +pdfToImage.includeAnnotations=Include annotations (comments, highlights etc.) #addPassword diff --git a/app/core/src/main/resources/messages_pt_PT.properties b/app/core/src/main/resources/messages_pt_PT.properties index 7b73092f1..9a750ef43 100644 --- a/app/core/src/main/resources/messages_pt_PT.properties +++ b/app/core/src/main/resources/messages_pt_PT.properties @@ -137,6 +137,7 @@ lang.yor=Yoruba addPageNumbers.fontSize=Tamanho da Fonte addPageNumbers.fontName=Nome da Fonte +addPageNumbers.fontColor=Font Colour pdfPrompt=Selecione PDF(s) multiPdfPrompt=Selecione PDFs (2+) multiPdfDropPrompt=Selecione (ou arraste e solte) todos os PDFs necessários @@ -193,6 +194,7 @@ error.fileFormatRequired=File must be in {0} format error.invalidFormat=Invalid {0} format: {1} error.endpointDisabled=This endpoint has been disabled by the admin error.urlNotReachable=URL is not reachable, please provide a valid URL +error.invalidUrlFormat=Invalid URL format provided. The provided format is invalid. # DPI and image rendering messages - used by frontend for dynamic translation # Backend sends: [TRANSLATE:messageKey:arg1,arg2] English message @@ -604,6 +606,22 @@ home.imageToPdf.title=Imagem para PDF home.imageToPdf.desc=Converter uma imagem (PNG, JPEG, GIF) para PDF. imageToPdf.tags=conversão,img,jpg,imagem,foto +home.cbzToPdf.title=CBZ to PDF +home.cbzToPdf.desc=Convert CBZ comic book archives to PDF format. +cbzToPdf.tags=conversion,comic,book,archive,cbz,zip + +home.cbrToPdf.title=CBR to PDF +home.cbrToPdf.desc=Convert CBR comic book archives to PDF format. +cbrToPdf.tags=conversion,comic,book,archive,cbr,rar + +home.pdfToCbz.title=PDF to CBZ +home.pdfToCbz.desc=Convert PDF files to CBZ comic book archives. +pdfToCbz.tags=conversion,comic,book,archive,cbz,pdf + +home.pdfToCbr.title=PDF to CBR +home.pdfToCbr.desc=Convert PDF files to CBR comic book archives. +pdfToCbr.tags=conversion,comic,book,archive,cbr,rar + home.pdfToImage.title=PDF para Imagem home.pdfToImage.desc=Converter um PDF para uma imagem. (PNG, JPEG, GIF) pdfToImage.tags=conversão,img,jpg,imagem,foto @@ -876,6 +894,12 @@ replace-color.selectText.8=Texto amarelo em fundo preto replace-color.selectText.9=Texto verde em fundo preto replace-color.selectText.10=Escolher cor do texto replace-color.selectText.11=Escolher cor do fundo +replace-color.selectText.12=Colour Space Conversion (CMYK for Printing) +replace-color.selectText.13=CMYK Colour Space Conversion +replace-color.selectText.14=This option converts the PDF from RGB colour space to CMYK colour space, which is optimized for professional printing. This process: +replace-color.selectText.15=Converts colours to CMYK (Cyan, Magenta, Yellow, Black) colour model used by professional printers +replace-color.selectText.16=Optimizes the PDF for print production with prepress settings +replace-color.selectText.17=May result in slight colour changes as CMYK has a smaller colour gamut than RGB replace-color.submit=Substituir @@ -1081,7 +1105,7 @@ addPageNumbers.selectText.5=Páginas a Numerar addPageNumbers.selectText.6=Texto Personalizado addPageNumbers.customTextDesc=Texto Personalizado addPageNumbers.numberPagesDesc=Quais páginas a numerar, predefinição 'todas', também aceita 1-5 ou 2,5,9 etc -addPageNumbers.customNumberDesc=Predefinição {n}, também aceita 'Página {n} de {total}', 'Texto-{n}', '{filename}-{n} +addPageNumbers.customNumberDesc=Predefinição {n}, também aceita 'Página {n} de {total}', 'Texto-{n}', '{filename}-{n}' addPageNumbers.submit=Adicionar Números de Página @@ -1217,6 +1241,7 @@ sign.previous=Página anterior sign.maintainRatio=Alternar manter proporção sign.undo=Undo sign.redo=Redo +sign.colour=Signature Colour #repair repair.title=Reparar @@ -1423,6 +1448,34 @@ imageToPDF.selectText.3=Lógica de múltiplos ficheiros (Apenas ativada se traba imageToPDF.selectText.4=Juntar num único PDF imageToPDF.selectText.5=Converter para PDFs separados +#cbzToPDF +cbzToPDF.title=CBZ to PDF +cbzToPDF.header=CBZ to PDF +cbzToPDF.submit=Convert to PDF +cbzToPDF.selectText=Select CBZ file +cbzToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBZ +pdfToCBZ.title=PDF to CBZ +pdfToCBZ.header=PDF to CBZ +pdfToCBZ.submit=Convert to CBZ +pdfToCBZ.selectText=Select PDF file +pdfToCBZ.dpi=DPI (Dots Per Inch) + +#cbrToPDF +cbrToPDF.title=CBR to PDF +cbrToPDF.header=CBR to PDF +cbrToPDF.submit=Convert to PDF +cbrToPDF.selectText=Select CBR file +cbrToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBR +pdfToCBR.title=PDF to CBR +pdfToCBR.header=PDF to CBR +pdfToCBR.submit=Convert to CBR +pdfToCBR.selectText=Select PDF file +pdfToCBR.dpi=DPI (Dots Per Inch) +pdfToCBR.dpiHelp=Higher DPI results in better quality but larger file size. #pdfToImage pdfToImage.title=PDF para Imagem @@ -1439,6 +1492,7 @@ pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Converter pdfToImage.info=Python não está instalado. Necessário para conversão WebP. pdfToImage.placeholder=(ex. 1,2,8 ou 4,7,12-16 ou 2n-1) +pdfToImage.includeAnnotations=Include annotations (comments, highlights etc.) #addPassword diff --git a/app/core/src/main/resources/messages_ro_RO.properties b/app/core/src/main/resources/messages_ro_RO.properties index 07fee9b86..9b14cab63 100644 --- a/app/core/src/main/resources/messages_ro_RO.properties +++ b/app/core/src/main/resources/messages_ro_RO.properties @@ -137,6 +137,7 @@ lang.yor=Yoruba addPageNumbers.fontSize=Dimensiune Font addPageNumbers.fontName=Nume Font +addPageNumbers.fontColor=Font Colour pdfPrompt=Selectează fișiere PDF multiPdfPrompt=Selectează mai multe fișiere PDF (2+) multiPdfDropPrompt=Selectează (sau trage și plasează) toate fișierele PDF de care ai nevoie @@ -193,6 +194,7 @@ error.fileFormatRequired=File must be in {0} format error.invalidFormat=Invalid {0} format: {1} error.endpointDisabled=This endpoint has been disabled by the admin error.urlNotReachable=URL is not reachable, please provide a valid URL +error.invalidUrlFormat=Invalid URL format provided. The provided format is invalid. # DPI and image rendering messages - used by frontend for dynamic translation # Backend sends: [TRANSLATE:messageKey:arg1,arg2] English message @@ -604,6 +606,22 @@ home.imageToPdf.title=Imagine în PDF home.imageToPdf.desc=Convertește o imagine (PNG, JPEG, GIF) în PDF. imageToPdf.tags=conversie,img,jpg,poză,fotografie +home.cbzToPdf.title=CBZ to PDF +home.cbzToPdf.desc=Convert CBZ comic book archives to PDF format. +cbzToPdf.tags=conversion,comic,book,archive,cbz,zip + +home.cbrToPdf.title=CBR to PDF +home.cbrToPdf.desc=Convert CBR comic book archives to PDF format. +cbrToPdf.tags=conversion,comic,book,archive,cbr,rar + +home.pdfToCbz.title=PDF to CBZ +home.pdfToCbz.desc=Convert PDF files to CBZ comic book archives. +pdfToCbz.tags=conversion,comic,book,archive,cbz,pdf + +home.pdfToCbr.title=PDF to CBR +home.pdfToCbr.desc=Convert PDF files to CBR comic book archives. +pdfToCbr.tags=conversion,comic,book,archive,cbr,rar + home.pdfToImage.title=PDF în Imagine home.pdfToImage.desc=Convertește un fișier PDF în imagine (PNG, JPEG, GIF). pdfToImage.tags=conversie,img,jpg,poză,fotografie @@ -876,6 +894,12 @@ replace-color.selectText.8=Yellow text on black background replace-color.selectText.9=Green text on black background replace-color.selectText.10=Choose text Color replace-color.selectText.11=Choose background Color +replace-color.selectText.12=Colour Space Conversion (CMYK for Printing) +replace-color.selectText.13=CMYK Colour Space Conversion +replace-color.selectText.14=This option converts the PDF from RGB colour space to CMYK colour space, which is optimized for professional printing. This process: +replace-color.selectText.15=Converts colours to CMYK (Cyan, Magenta, Yellow, Black) colour model used by professional printers +replace-color.selectText.16=Optimizes the PDF for print production with prepress settings +replace-color.selectText.17=May result in slight colour changes as CMYK has a smaller colour gamut than RGB replace-color.submit=Replace @@ -1081,7 +1105,7 @@ addPageNumbers.selectText.5=Pagini de Numerotat addPageNumbers.selectText.6=Text Personalizat addPageNumbers.customTextDesc=Text Personalizat addPageNumbers.numberPagesDesc=Ce pagini să numeroteze, implicit 'toate', acceptă și 1-5 sau 2,5,9 etc -addPageNumbers.customNumberDesc=Implicit la {n}, acceptă și 'Pagina {n} din {total}', 'Text-{n}', '{nume_fisier}-{n} +addPageNumbers.customNumberDesc=Implicit la {n}, acceptă și 'Pagina {n} din {total}', 'Text-{n}', '{filename}-{n}' addPageNumbers.submit=Adaugă Numere de Pagină @@ -1217,6 +1241,7 @@ sign.previous=Previous page sign.maintainRatio=Toggle maintain aspect ratio sign.undo=Undo sign.redo=Redo +sign.colour=Signature Colour #repair repair.title=Repară @@ -1423,6 +1448,34 @@ imageToPDF.selectText.3=Logica pentru mai multe fișiere (activată numai dacă imageToPDF.selectText.4=Unifică într-un singur PDF imageToPDF.selectText.5=Convertește în PDF-uri separate +#cbzToPDF +cbzToPDF.title=CBZ to PDF +cbzToPDF.header=CBZ to PDF +cbzToPDF.submit=Convert to PDF +cbzToPDF.selectText=Select CBZ file +cbzToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBZ +pdfToCBZ.title=PDF to CBZ +pdfToCBZ.header=PDF to CBZ +pdfToCBZ.submit=Convert to CBZ +pdfToCBZ.selectText=Select PDF file +pdfToCBZ.dpi=DPI (Dots Per Inch) + +#cbrToPDF +cbrToPDF.title=CBR to PDF +cbrToPDF.header=CBR to PDF +cbrToPDF.submit=Convert to PDF +cbrToPDF.selectText=Select CBR file +cbrToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBR +pdfToCBR.title=PDF to CBR +pdfToCBR.header=PDF to CBR +pdfToCBR.submit=Convert to CBR +pdfToCBR.selectText=Select PDF file +pdfToCBR.dpi=DPI (Dots Per Inch) +pdfToCBR.dpiHelp=Higher DPI results in better quality but larger file size. #pdfToImage pdfToImage.title=PDF în Imagine @@ -1439,6 +1492,7 @@ pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Convertește pdfToImage.info=Python nu este instalat. Necesar pentru conversia WebP. pdfToImage.placeholder=(ex. 1,2,8 sau 4,7,12-16 sau 2n-1) +pdfToImage.includeAnnotations=Include annotations (comments, highlights etc.) #addPassword diff --git a/app/core/src/main/resources/messages_ru_RU.properties b/app/core/src/main/resources/messages_ru_RU.properties index 14dd4121a..fb0494bb4 100644 --- a/app/core/src/main/resources/messages_ru_RU.properties +++ b/app/core/src/main/resources/messages_ru_RU.properties @@ -137,6 +137,7 @@ lang.yor=Yoruba addPageNumbers.fontSize=Размер шрифта addPageNumbers.fontName=Название шрифта +addPageNumbers.fontColor=Font Colour pdfPrompt=Выберите PDF-файл(ы) multiPdfPrompt=Выберите PDF-файлы (2+) multiPdfDropPrompt=Выберите (или перетащите) все необходимые PDF-файлы @@ -193,6 +194,7 @@ error.fileFormatRequired=Файл должен быть в формате {0} error.invalidFormat=Недопустимый формат {0}: {1} error.endpointDisabled=Эта конечная точка была отключена администратором error.urlNotReachable=URL-адрес недоступен, пожалуйста, укажите действительный URL-адрес +error.invalidUrlFormat=Invalid URL format provided. The provided format is invalid. # DPI and image rendering messages - used by frontend for dynamic translation # Backend sends: [TRANSLATE:messageKey:arg1,arg2] English message @@ -368,36 +370,36 @@ settings.update=Доступно обновление settings.updateAvailable=Текущая установленная версия - {0}. Доступна новая версия ({1}). # Update modal and notification strings -update.urgentUpdateAvailable=🚨 Update Available -update.updateAvailable=Update Available -update.modalTitle=Update Available -update.current=Current -update.latest=Latest -update.latestStable=Latest Stable -update.priority=Priority -update.recommendedAction=Recommended Action -update.breakingChangesDetected=⚠️ Breaking Changes Detected -update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. -update.migrationGuides=Migration Guides: -update.viewGuide=View Guide -update.loadingDetailedInfo=Loading detailed version information... -update.close=Close -update.viewAllReleases=View All Releases -update.downloadLatest=Download Latest -update.availableUpdates=Available Updates: -update.unableToLoadDetails=Unable to load detailed version information. -update.version=Version +update.urgentUpdateAvailable=🚨 Доступно обновление +update.updateAvailable=Доступно обновление +update.modalTitle=Доступно обновление +update.current=Текущая +update.latest=Последняя +update.latestStable=Последняя стабильная версия +update.priority=Приоритетное +update.recommendedAction=Рекомендуемые действия +update.breakingChangesDetected=⚠️ Обнаружены критические изменения +update.breakingChangesMessage=Это обновление содержит важные изменения. Пожалуйста, ознакомьтесь с приведенными ниже руководствами по переносу. +update.migrationGuides=Руководства по миграции: +update.viewGuide=Посмотреть руководство +update.loadingDetailedInfo=Загрузка подробной информации о версии... +update.close=Закрыть +update.viewAllReleases=Просмотреть все релизы +update.downloadLatest=Скачать последнюю версию +update.availableUpdates=Доступные обновления: +update.unableToLoadDetails=Не удается загрузить подробную информацию о версии. +update.version=Версия # Update priority levels -update.priority.urgent=URGENT -update.priority.normal=NORMAL -update.priority.minor=MINOR -update.priority.low=LOW +update.priority.urgent=СРОЧНЫЙ +update.priority.normal=НОРМАЛЬНЫЙ +update.priority.minor=НЕЗНАЧИТЕЛЬНЫЙ +update.priority.low=НИЗШИЙ # Breaking changes text -update.breakingChanges=Breaking Changes: -update.breakingChangesDefault=This version contains breaking changes -update.migrationGuide=Migration Guide +update.breakingChanges=Важные изменения: +update.breakingChangesDefault=Эта версия содержит важные изменения +update.migrationGuide=Руководство по миграции settings.appVersion=Версия приложения: settings.downloadOption.title=Выберите вариант загрузки (для одиночных файлов без архивации): settings.downloadOption.1=Открыть в том же окне @@ -406,7 +408,7 @@ settings.downloadOption.3=Скачать файл settings.zipThreshold=Архивировать файлы, когда количество загружаемых файлов превышает settings.signOut=Выйти settings.accountSettings=Настройки аккаунта -settings.bored.help=Включает пасхалку-игру +settings.bored.help=Включает игру-пасхалку settings.cacheInputs.name=Сохранять данные форм settings.cacheInputs.help=Включите для сохранения ранее использованных данных для будущих запусков @@ -604,6 +606,22 @@ home.imageToPdf.title=Изображение в PDF home.imageToPdf.desc=Преобразование изображения (PNG, JPEG, GIF) в PDF. imageToPdf.tags=png,jpeg,gif,конвертация,изображение,картинка,фото +home.cbzToPdf.title=CBZ в PDF +home.cbzToPdf.desc=Конвертируйте архивы комиксов CBZ в формат PDF. +cbzToPdf.tags=конвертация,комикс,книга,архив,cbz,pdf + +home.cbrToPdf.title=CBR to PDF +home.cbrToPdf.desc=Convert CBR comic book archives to PDF format. +cbrToPdf.tags=conversion,comic,book,archive,cbr,rar + +home.pdfToCbz.title=PDF в CBZ +home.pdfToCbz.desc=Конвертируйте PDF-файлы в архивы комиксов CBZ. +pdfToCbz.tags=конвертация,комикс,книга,архив,cbz,pdf + +home.pdfToCbr.title=PDF to CBR +home.pdfToCbr.desc=Convert PDF files to CBR comic book archives. +pdfToCbr.tags=conversion,comic,book,archive,cbr,rar + home.pdfToImage.title=PDF в изображение home.pdfToImage.desc=Преобразование PDF в изображение (PNG, JPEG, GIF). pdfToImage.tags=png,jpeg,gif,конвертация,изображение,картинка,фото @@ -876,6 +894,12 @@ replace-color.selectText.8=жёлтый текст на чёрном фоне replace-color.selectText.9=зелёный текст на чёрном фоне replace-color.selectText.10=Выбрать цвет текста replace-color.selectText.11=Выбрать цвет фона +replace-color.selectText.12=Преобразование цветового пространства (CMYK для печати) +replace-color.selectText.13=Преобразование цветового пространства CMYK +replace-color.selectText.14=Эта опция преобразует PDF-файл из цветового пространства RGB в цветовое пространство CMYK, оптимизированное для профессиональной печати. Этот процесс: +replace-color.selectText.15=Преобразует цвета в цветовую модель CMYK (голубой, пурпурный, желтый, черный), используемую профессиональными принтерами +replace-color.selectText.16=Оптимизирует PDF-файл для печати с помощью настроек допечатной подготовки +replace-color.selectText.17=Может привести к незначительным изменениям цвета, поскольку цветовая гамма CMYK меньше, чем RGB replace-color.submit=Заменить @@ -908,7 +932,7 @@ login.alreadyLoggedIn=Вы уже вошли в login.alreadyLoggedIn2=устройств(а). Пожалуйста, выйдите из этих устройств и попробуйте снова. login.toManySessions=У вас слишком много активных сессий login.logoutMessage=Вы вышли из системы. -login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator. +login.invalidInResponseTo=Запрошенный SAML-ответ недействителен или срок его действия истек. Пожалуйста, свяжитесь с администратором. #auto-redact autoRedact.title=Автоматическое редактирование @@ -1217,6 +1241,7 @@ sign.previous=Предыдущая страница sign.maintainRatio=Переключить сохранение пропорций sign.undo=Отменить sign.redo=Повторить +sign.colour=Цвет подписи #repair repair.title=Восстановление @@ -1423,6 +1448,34 @@ imageToPDF.selectText.3=Логика обработки множества фа imageToPDF.selectText.4=Объединить в один PDF imageToPDF.selectText.5=Преобразовать в отдельные PDF +#cbzToPDF +cbzToPDF.title=CBZ в PDF +cbzToPDF.header=CBZ в PDF +cbzToPDF.submit=Конвертация в PDF +cbzToPDF.selectText=Выбрать файл CBZ +cbzToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBZ +pdfToCBZ.title=PDF в CBZ +pdfToCBZ.header=PDF в CBZ +pdfToCBZ.submit=Конвертация в CBZ +pdfToCBZ.selectText=Выбрать файл PDF +pdfToCBZ.dpi=DPI (точек на дюйм) + +#cbrToPDF +cbrToPDF.title=CBR to PDF +cbrToPDF.header=CBR to PDF +cbrToPDF.submit=Convert to PDF +cbrToPDF.selectText=Select CBR file +cbrToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBR +pdfToCBR.title=PDF to CBR +pdfToCBR.header=PDF to CBR +pdfToCBR.submit=Convert to CBR +pdfToCBR.selectText=Select PDF file +pdfToCBR.dpi=DPI (Dots Per Inch) +pdfToCBR.dpiHelp=Higher DPI results in better quality but larger file size. #pdfToImage pdfToImage.title=PDF в изображение @@ -1435,10 +1488,11 @@ pdfToImage.colorType=Тип цвета pdfToImage.color=Цветной pdfToImage.grey=Оттенки серого pdfToImage.blackwhite=Черно-белый (возможна потеря данных!) -pdfToImage.dpi=DPI (The server limit is {0} dpi) +pdfToImage.dpi=DPI (ограничение сервера {0} dpi) pdfToImage.submit=Преобразовать pdfToImage.info=Python не установлен. Требуется для конвертации в WebP. pdfToImage.placeholder=(например, 1,2,8 или 4,7,12-16 или 2n-1) +pdfToImage.includeAnnotations=Добавьте аннотации (комментарии, основные моменты и т.д.). #addPassword @@ -1893,12 +1947,12 @@ editTableOfContents.replaceExisting=Заменить существующие з editTableOfContents.editorTitle=Редактор закладок editTableOfContents.editorDesc=Добавьте и упорядочьте закладки ниже. Нажмите «+», чтобы добавить дочерние закладки. editTableOfContents.addBookmark=Добавить новую закладку -editTableOfContents.importBookmarksDefault=Import -editTableOfContents.importBookmarksFromJsonFile=Upload JSON file -editTableOfContents.importBookmarksFromClipboard=Paste from clipboard -editTableOfContents.exportBookmarksDefault=Export -editTableOfContents.exportBookmarksAsJson=Download as JSON -editTableOfContents.exportBookmarksAsText=Copy as text +editTableOfContents.importBookmarksDefault=Импортировать +editTableOfContents.importBookmarksFromJsonFile=Загрузить из JSON файл +editTableOfContents.importBookmarksFromClipboard=Вставить из буфера обмена +editTableOfContents.exportBookmarksDefault=Экспортировать +editTableOfContents.exportBookmarksAsJson=Скачать в формате JSON +editTableOfContents.exportBookmarksAsText=Копировать в виде текста editTableOfContents.desc.1=Этот инструмент позволяет вам добавлять или редактировать оглавление (закладки) в PDF-документе. editTableOfContents.desc.2=Вы можете создать иерархическую структуру, добавив дочерние закладки к родительским. editTableOfContents.desc.3=Для каждой закладки требуется название и номер целевой страницы. diff --git a/app/core/src/main/resources/messages_sk_SK.properties b/app/core/src/main/resources/messages_sk_SK.properties index 4b84511f5..d490a752f 100644 --- a/app/core/src/main/resources/messages_sk_SK.properties +++ b/app/core/src/main/resources/messages_sk_SK.properties @@ -137,6 +137,7 @@ lang.yor=Yoruba addPageNumbers.fontSize=Font Size addPageNumbers.fontName=Font Name +addPageNumbers.fontColor=Font Colour pdfPrompt=Vyberte PDF súbor(y) multiPdfPrompt=Vyberte PDF súbory (2+) multiPdfDropPrompt=Vyberte (alebo pretiahnite) všetky požadované PDF súbory @@ -193,6 +194,7 @@ error.fileFormatRequired=File must be in {0} format error.invalidFormat=Invalid {0} format: {1} error.endpointDisabled=This endpoint has been disabled by the admin error.urlNotReachable=URL is not reachable, please provide a valid URL +error.invalidUrlFormat=Invalid URL format provided. The provided format is invalid. # DPI and image rendering messages - used by frontend for dynamic translation # Backend sends: [TRANSLATE:messageKey:arg1,arg2] English message @@ -604,6 +606,22 @@ home.imageToPdf.title=Obrázok na PDF home.imageToPdf.desc=Konvertujte obrázok (PNG, JPEG, GIF) na PDF. imageToPdf.tags=konverzia,img,jpg,obrázok,fotografia +home.cbzToPdf.title=CBZ to PDF +home.cbzToPdf.desc=Convert CBZ comic book archives to PDF format. +cbzToPdf.tags=conversion,comic,book,archive,cbz,zip + +home.cbrToPdf.title=CBR to PDF +home.cbrToPdf.desc=Convert CBR comic book archives to PDF format. +cbrToPdf.tags=conversion,comic,book,archive,cbr,rar + +home.pdfToCbz.title=PDF to CBZ +home.pdfToCbz.desc=Convert PDF files to CBZ comic book archives. +pdfToCbz.tags=conversion,comic,book,archive,cbz,pdf + +home.pdfToCbr.title=PDF to CBR +home.pdfToCbr.desc=Convert PDF files to CBR comic book archives. +pdfToCbr.tags=conversion,comic,book,archive,cbr,rar + home.pdfToImage.title=PDF na obrázok home.pdfToImage.desc=Konvertujte PDF na obrázok. (PNG, JPEG, GIF) pdfToImage.tags=konverzia,img,jpg,obrázok,fotografia @@ -876,6 +894,12 @@ replace-color.selectText.8=Yellow text on black background replace-color.selectText.9=Green text on black background replace-color.selectText.10=Choose text Color replace-color.selectText.11=Choose background Color +replace-color.selectText.12=Colour Space Conversion (CMYK for Printing) +replace-color.selectText.13=CMYK Colour Space Conversion +replace-color.selectText.14=This option converts the PDF from RGB colour space to CMYK colour space, which is optimized for professional printing. This process: +replace-color.selectText.15=Converts colours to CMYK (Cyan, Magenta, Yellow, Black) colour model used by professional printers +replace-color.selectText.16=Optimizes the PDF for print production with prepress settings +replace-color.selectText.17=May result in slight colour changes as CMYK has a smaller colour gamut than RGB replace-color.submit=Replace @@ -1081,7 +1105,7 @@ addPageNumbers.selectText.5=Stránky na číslovanie addPageNumbers.selectText.6=Vlastný text addPageNumbers.customTextDesc=Vlastný text addPageNumbers.numberPagesDesc=Ktoré stránky číslovať, predvolené 'všetky', tiež akceptuje 1-5 alebo 2,5,9 atď. -addPageNumbers.customNumberDesc=Predvolené {n}, tiež akceptuje 'Strana {n} z {total}', 'Text-{n}', '{filename}-{n} +addPageNumbers.customNumberDesc=Predvolené {n}, tiež akceptuje 'Strana {n} z {total}', 'Text-{n}', '{filename}-{n}' addPageNumbers.submit=Pridať čísla stránok @@ -1217,6 +1241,7 @@ sign.previous=Previous page sign.maintainRatio=Toggle maintain aspect ratio sign.undo=Undo sign.redo=Redo +sign.colour=Signature Colour #repair repair.title=Opraviť @@ -1423,6 +1448,34 @@ imageToPDF.selectText.3=Logika pre viac súborov (Povolí sa len, ak pracujete s imageToPDF.selectText.4=Zlúčiť do jedného PDF imageToPDF.selectText.5=Konvertovať na samostatné PDF +#cbzToPDF +cbzToPDF.title=CBZ to PDF +cbzToPDF.header=CBZ to PDF +cbzToPDF.submit=Convert to PDF +cbzToPDF.selectText=Select CBZ file +cbzToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBZ +pdfToCBZ.title=PDF to CBZ +pdfToCBZ.header=PDF to CBZ +pdfToCBZ.submit=Convert to CBZ +pdfToCBZ.selectText=Select PDF file +pdfToCBZ.dpi=DPI (Dots Per Inch) + +#cbrToPDF +cbrToPDF.title=CBR to PDF +cbrToPDF.header=CBR to PDF +cbrToPDF.submit=Convert to PDF +cbrToPDF.selectText=Select CBR file +cbrToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBR +pdfToCBR.title=PDF to CBR +pdfToCBR.header=PDF to CBR +pdfToCBR.submit=Convert to CBR +pdfToCBR.selectText=Select PDF file +pdfToCBR.dpi=DPI (Dots Per Inch) +pdfToCBR.dpiHelp=Higher DPI results in better quality but larger file size. #pdfToImage pdfToImage.title=PDF na obrázok @@ -1439,6 +1492,7 @@ pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Konvertovať pdfToImage.info=Python is not installed. Required for WebP conversion. pdfToImage.placeholder=(napr. 1,2,8 alebo 4,7,12-16 alebo 2n-1) +pdfToImage.includeAnnotations=Include annotations (comments, highlights etc.) #addPassword diff --git a/app/core/src/main/resources/messages_sl_SI.properties b/app/core/src/main/resources/messages_sl_SI.properties index 72987dfcd..1173aae96 100644 --- a/app/core/src/main/resources/messages_sl_SI.properties +++ b/app/core/src/main/resources/messages_sl_SI.properties @@ -137,6 +137,7 @@ lang.yor=Yoruba addPageNumbers.fontSize=Velikost pisave addPageNumbers.fontName=Ime pisave +addPageNumbers.fontColor=Font Colour pdfPrompt=Izberi PDF(e) multiPdfPrompt=Izberi PDF (2+) multiPdfDropPrompt=Izberite (ali povlecite in spustite) vse datoteke PDF, ki jih potrebujete @@ -193,6 +194,7 @@ error.fileFormatRequired=File must be in {0} format error.invalidFormat=Invalid {0} format: {1} error.endpointDisabled=This endpoint has been disabled by the admin error.urlNotReachable=URL is not reachable, please provide a valid URL +error.invalidUrlFormat=Invalid URL format provided. The provided format is invalid. # DPI and image rendering messages - used by frontend for dynamic translation # Backend sends: [TRANSLATE:messageKey:arg1,arg2] English message @@ -604,6 +606,22 @@ home.imageToPdf.title=Slika v PDF home.imageToPdf.desc=Pretvori sliko (PNG, JPEG, GIF) v PDF. imageToPdf.tags=pretvorba,img,jpg,slika,fotografija +home.cbzToPdf.title=CBZ to PDF +home.cbzToPdf.desc=Convert CBZ comic book archives to PDF format. +cbzToPdf.tags=conversion,comic,book,archive,cbz,zip + +home.cbrToPdf.title=CBR to PDF +home.cbrToPdf.desc=Convert CBR comic book archives to PDF format. +cbrToPdf.tags=conversion,comic,book,archive,cbr,rar + +home.pdfToCbz.title=PDF to CBZ +home.pdfToCbz.desc=Convert PDF files to CBZ comic book archives. +pdfToCbz.tags=conversion,comic,book,archive,cbz,pdf + +home.pdfToCbr.title=PDF to CBR +home.pdfToCbr.desc=Convert PDF files to CBR comic book archives. +pdfToCbr.tags=conversion,comic,book,archive,cbr,rar + home.pdfToImage.title=PDF v sliko home.pdfToImage.desc=Pretvori PDF v sliko. (PNG, JPEG, GIF) pdfToImage.tags=pretvorba,img,jpg,slika,fotografija @@ -876,6 +894,12 @@ replace-color.selectText.8=Rumeno besedilo na črnem ozadju replace-color.selectText.9=Zeleno besedilo na črnem ozadju replace-color.selectText.10=Izberi barvo besedila replace-color.selectText.11=Izberi barvo ozadja +replace-color.selectText.12=Colour Space Conversion (CMYK for Printing) +replace-color.selectText.13=CMYK Colour Space Conversion +replace-color.selectText.14=This option converts the PDF from RGB colour space to CMYK colour space, which is optimized for professional printing. This process: +replace-color.selectText.15=Converts colours to CMYK (Cyan, Magenta, Yellow, Black) colour model used by professional printers +replace-color.selectText.16=Optimizes the PDF for print production with prepress settings +replace-color.selectText.17=May result in slight colour changes as CMYK has a smaller colour gamut than RGB replace-color.submit=Zamenjaj @@ -1081,7 +1105,7 @@ addPageNumbers.selectText.5=Strani v številko addPageNumbers.selectText.6=Besedilo po meri addPageNumbers.customTextDesc=Besedilo po meri addPageNumbers.numberPagesDesc=Katere strani oštevilčiti, privzeto 'vse', sprejema tudi 1-5 ali 2,5,9 itd. -addPageNumbers.customNumberDesc=Privzeto na {n}, sprejema tudi 'Stran {n} od {total}', 'Besedilo-{n}', '{filename}-{n} +addPageNumbers.customNumberDesc=Privzeto na {n}, sprejema tudi 'Stran {n} od {total}', 'Besedilo-{n}', '{filename}-{n}' addPageNumbers.submit=Dodaj številke strani @@ -1217,6 +1241,7 @@ sign.previous=Prejšnja stran sign.maintainRatio=Preklopi ohranjanje razmerja stranic sign.undo=Razveljavi sign.redo=Ponovi +sign.colour=Signature Colour #repair repair.title=Popravilo @@ -1423,6 +1448,34 @@ imageToPDF.selectText.3=Logika več datotek (omogočeno samo pri delu z več sli imageToPDF.selectText.4=Združi v en PDF imageToPDF.selectText.5=Pretvori v ločene datoteke PDF +#cbzToPDF +cbzToPDF.title=CBZ to PDF +cbzToPDF.header=CBZ to PDF +cbzToPDF.submit=Convert to PDF +cbzToPDF.selectText=Select CBZ file +cbzToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBZ +pdfToCBZ.title=PDF to CBZ +pdfToCBZ.header=PDF to CBZ +pdfToCBZ.submit=Convert to CBZ +pdfToCBZ.selectText=Select PDF file +pdfToCBZ.dpi=DPI (Dots Per Inch) + +#cbrToPDF +cbrToPDF.title=CBR to PDF +cbrToPDF.header=CBR to PDF +cbrToPDF.submit=Convert to PDF +cbrToPDF.selectText=Select CBR file +cbrToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBR +pdfToCBR.title=PDF to CBR +pdfToCBR.header=PDF to CBR +pdfToCBR.submit=Convert to CBR +pdfToCBR.selectText=Select PDF file +pdfToCBR.dpi=DPI (Dots Per Inch) +pdfToCBR.dpiHelp=Higher DPI results in better quality but larger file size. #pdfToImage pdfToImage.title=PDF v sliko @@ -1439,6 +1492,7 @@ pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Pretvori pdfToImage.info=Python ni nameščen. Zahtevano za pretvorbo WebP. pdfToImage.placeholder=(npr. 1,2,8 ali 4,7,12-16 ali 2n-1) +pdfToImage.includeAnnotations=Include annotations (comments, highlights etc.) #addPassword diff --git a/app/core/src/main/resources/messages_sr_LATN_RS.properties b/app/core/src/main/resources/messages_sr_LATN_RS.properties index 4a6e987ca..5313a4582 100644 --- a/app/core/src/main/resources/messages_sr_LATN_RS.properties +++ b/app/core/src/main/resources/messages_sr_LATN_RS.properties @@ -137,6 +137,7 @@ lang.yor=Joruba addPageNumbers.fontSize=Veličina fonta addPageNumbers.fontName=Naziv fonta +addPageNumbers.fontColor=Boja fonta pdfPrompt=Odaberi PDF(ove) multiPdfPrompt=Odaberi PDF-ove (2+) multiPdfDropPrompt=Odaberi (ili prevuci i pusti) sve PDF-ove koji su ti potrebni @@ -155,7 +156,7 @@ unknown=Nepoznato save=Sačuvaj saveToBrowser=Sačuvaj u pregledaču close=Zatvori -filesSelected=odabrani fajlovi +filesSelected=odabrane datoteke noFavourites=Nema dodatih favorita downloadComplete=Preuzimanje završeno bored=Da li ti je dosadno dok čekaš? @@ -170,67 +171,68 @@ sizes.medium=Srednje sizes.large=Veliko sizes.x-large=X-Veliko error.pdfPassword=PDF dokument je šifrovan i lozinka nije data ili je netačna -error.pdfCorrupted=PDF file appears to be corrupted or damaged. Please try using the 'Repair PDF' feature first to fix the file before proceeding with this operation. -error.pdfCorruptedMultiple=One or more PDF files appear to be corrupted or damaged. Please try using the 'Repair PDF' feature on each file first before attempting to merge them. -error.pdfCorruptedDuring=Error {0}: PDF file appears to be corrupted or damaged. Please try using the 'Repair PDF' feature first to fix the file before proceeding with this operation. +error.pdfCorrupted=PDF dokument je izgleda oštećen ili neispravan. Pokušaj prvo sa opcijom 'Popravi PDF' da ga popraviš, pre nego što nastaviš sa ovom operacijom. +error.pdfCorruptedMultiple=Jedan ili više PDF dokumenata su izgleda oštećeni ili neispravni. Pokušaj da iskoristiš opciju 'Popravi PDF' na svakom dokumentu, pre pokušaja spajanja. +error.pdfCorruptedDuring=Greška {0}: PDF dokument je izgleda oštećen ili neispravan. Pokušaj prvo sa opcijom 'Popravi PDF' da ga popraviš, pre nego što nastaviš sa ovom operacijom. # Frontend corruption error messages -error.pdfInvalid=The PDF file "{0}" appears to be corrupted or has an invalid structure. Please try using the 'Repair PDF' feature to fix the file before proceeding. -error.tryRepair=Try using the Repair PDF feature to fix corrupted files. +error.pdfInvalid=The PDF file "{0}" je izgleda oštećen ili neispravan. Pokušaj prvo sa opcijom 'Popravi PDF' da ga popraviš, pre nego što nastaviš. +error.tryRepair=Pokušaj korišćenjem opcije 'Popravi PDF' da popraviš oštećene dokumente. # Additional error messages -error.pdfEncryption=The PDF appears to have corrupted encryption data. This can happen when the PDF was created with incompatible encryption methods. Please try using the 'Repair PDF' feature first, or contact the document creator for a new copy. -error.fileProcessing=An error occurred while processing the file during {0} operation: {1} +error.pdfEncryption=PDF dokument izgleda sadrži neispravne enkriptovane podatke. Ovo može da se desi kada se koristi nekompatibilna metoda enkripcije. Pokušaj prvo da ga popraviš korišćenjem opcije 'Popravi PDF', ili zatraži od kreatora dokumenta novu kopiju. +error.fileProcessing=Došlo je do greške prilikom obrade datoteke tokom {0} operacije: {1} # Generic error message templates -error.toolNotInstalled={0} is not installed -error.toolRequired={0} is required for {1} -error.conversionFailed={0} conversion failed -error.commandFailed={0} command failed -error.algorithmNotAvailable={0} algorithm not available -error.optionsNotSpecified={0} options are not specified -error.fileFormatRequired=File must be in {0} format -error.invalidFormat=Invalid {0} format: {1} -error.endpointDisabled=This endpoint has been disabled by the admin -error.urlNotReachable=URL is not reachable, please provide a valid URL +error.toolNotInstalled={0} nije instaliran +error.toolRequired={0} je neophodan za {1} +error.conversionFailed={0} konverzija nije uspela +error.commandFailed={0} komanda nije uspela +error.algorithmNotAvailable={0} algoritam nije raspoloživ +error.optionsNotSpecified={0} opcije nisu precizirane +error.fileFormatRequired=Fajl mora biti u {0} formatu +error.invalidFormat=Nevažeći {0} format: {1} +error.endpointDisabled=Ova krajnja tačka je onemogućena od strane administratora +error.urlNotReachable=URL nije dostupan, upišite validan URL +error.invalidUrlFormat=Upisan neispravan URL format. Dostavljeni format nije ispravan. # DPI and image rendering messages - used by frontend for dynamic translation # Backend sends: [TRANSLATE:messageKey:arg1,arg2] English message # Frontend parses this and replaces with localized versions using these keys -error.dpiExceedsLimit=DPI value {0} exceeds maximum safe limit of {1}. High DPI values can cause memory issues and crashes. Please use a lower DPI value. -error.pageTooBigForDpi=PDF page {0} is too large to render at {1} DPI. Please try a lower DPI value (recommended: 150 or less). -error.pageTooBigExceedsArray=PDF page {0} is too large to render at {1} DPI. The resulting image would exceed Java's maximum array size. Please try a lower DPI value (recommended: 150 or less). -error.pageTooBigFor300Dpi=PDF page {0} is too large to render at 300 DPI. The resulting image would exceed Java's maximum array size. Please use a lower DPI value for PDF-to-image conversion. +error.dpiExceedsLimit=DPI vrednost {0} veća od maksimalno bezbednog limita od {1}. Visoke vrednosti DPI mogu dovesti da problema sa memorijom i pucanja programa. Koristi manju DPI vrednost. +error.pageTooBigForDpi=PDF stranica {0} je prevelika za renderovanje u {1} DPI. Koristi nižu DPI vrednost (preporučeno: 150 ili manje). +error.pageTooBigExceedsArray=PDF stranica {0} je prevelika za renderovanje u {1} DPI. Rezultujuća slika bi premašila maksimalnu veličinu niza koju podržava Java. Pokušaj sa manjom DPI vrednošću (preporučeno: 150 ili manje). +error.pageTooBigFor300Dpi=PDF stranica {0} je prevelika za renderovanje u 300 DPI. Rezultujuća slika bi premašila maksimalnu veličinu niza koju podržava Java. Koristi nižu DPI vrednost za konverziju 'PDF u sliku'. # URL and website conversion messages # System requirements messages # Authentication and security messages -error.apiKeyInvalid=API key is not valid. -error.userNotFound=User not found. -error.passwordRequired=Password must not be null. -error.accountLocked=Your account has been locked due to too many failed login attempts. -error.invalidEmail=Invalid email addresses provided. -error.emailAttachmentRequired=An attachment is required to send the email. -error.signatureNotFound=Signature file not found. +error.apiKeyInvalid=API ključ je nevažeći. +error.userNotFound=Nepostojeći korisnik. +error.passwordRequired=Lozinka ne sme biti prazna. +error.accountLocked=Nalog je zaključan usled previše neuspelih pokušaja prijave. +error.invalidEmail=Unete su nevažeće email adrese. +error.emailAttachmentRequired=Za slanje emaila neophodno je priložiti datoteku. +error.signatureNotFound=Datoteka sa potpisom nije pronađena. # File processing messages -error.fileNotFound=File not found with ID: {0} +error.fileNotFound=Datoteka sa ID-jem {0} nije pronađen # Database and configuration messages -error.noBackupScripts=No backup scripts were found. -error.unsupportedProvider={0} is not currently supported. -error.pathTraversalDetected=Path traversal detected for security reasons. +error.noBackupScripts=Skripte za pravljene rezervne kopije nisu pronađene. +error.unsupportedProvider={0} nije trenutno podržan. +error.pathTraversalDetected=Zahtev je blokiran zbog pokušaja nebezbednog pristupa putanji. # Validation messages -error.invalidArgument=Invalid argument: {0} -error.argumentRequired={0} must not be null -error.operationFailed=Operation failed: {0} -error.angleNotMultipleOf90=Angle must be a multiple of 90 -error.pdfBookmarksNotFound=No PDF bookmarks/outline found in document -error.fontLoadingFailed=Error processing font file -error.fontDirectoryReadFailed=Failed to read font directory +error.invalidArgument=Nevažeći argument: {0} +error.argumentRequired={0} ne sme biti prazan +error.operationFailed=Operacije nije uspela: {0} +error.angleNotMultipleOf90=Vrednost ugla mora biti deljiva sa 90 +error.pdfBookmarksNotFound=PDF dokument ne sadrži obeleživače/navigacioni sadržaj +error.fontLoadingFailed=Greška prilikom obrade fonta +error.fontDirectoryReadFailed=Greška prilikom pristupa direktorijuma sa fontovima delete=Obriši username=Korisničko ime password=Lozinka @@ -246,7 +248,7 @@ poweredBy=Omogućeno od strane yes=Da no=Ne changedCredsMessage=Podaci za prijavu uspešno promenjeni! -notAuthenticatedMessage=Korisnik nije autentifikovan. +notAuthenticatedMessage=Korisnik nije prijavljen. userNotFoundMessage=Korisnik nije pronađen. incorrectPasswordMessage=Trenutna lozinka je netačna. usernameExistsMessage=Novi korisnik već postoji @@ -267,7 +269,7 @@ help=Pomoć goHomepage=Idi na početnu stranu joinDiscord=Pridružite se našem Discord serveru seeDockerHub=Pogledaj Docker Hub -visitGithub=Poseti Github repozitorijum +visitGithub=Poseti GitHub repozitorijum donate=Doniraj color=Boja sponsor=Sponzor @@ -334,7 +336,7 @@ enterpriseEdition.proTeamFeatureDisabled=Funkcije upravljanja timom zahtevaju Pr # Analytics # ################# analytics.title=Želiš li da učiniš Stirling PDF boljim? -analytics.paragraph1=Stirling PDF ima opcioni sistem analitike koji nam pomaže da unapredimo proizvod. Ne pratimo nikakve lične podatke niti sadržaj fajlova. +analytics.paragraph1=Stirling PDF ima opcioni sistem analitike koji nam pomaže da unapredimo proizvod. Ne pratimo nikakve lične podatke niti sadržaj datoteka. analytics.paragraph2=Molimo te da razmotriš uključivanje analitike kako bi pomogao Stirling PDF-u da raste i omogućio nam bolje razumevanje naših korisnika. analytics.enable=Omogući analitiku analytics.disable=Onemogući analitiku @@ -350,7 +352,7 @@ navbar.darkmode=Tamni režim navbar.language=Jezici navbar.settings=Podešavanja navbar.allTools=Alati -navbar.multiTool=Višefunkcijski alati +navbar.multiTool=Multifunkcijski alati navbar.search=Pretraga navbar.sections.organize=Organizacija navbar.sections.convertTo=Konvertuj u PDF @@ -368,38 +370,38 @@ settings.update=Dostupno ažuriranje settings.updateAvailable={0} je trenutno instalirana verzija. Nova verzija ({1}) je dostupna. # Update modal and notification strings -update.urgentUpdateAvailable=🚨 Update Available -update.updateAvailable=Update Available -update.modalTitle=Update Available -update.current=Current -update.latest=Latest -update.latestStable=Latest Stable -update.priority=Priority -update.recommendedAction=Recommended Action -update.breakingChangesDetected=⚠️ Breaking Changes Detected -update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. -update.migrationGuides=Migration Guides: -update.viewGuide=View Guide -update.loadingDetailedInfo=Loading detailed version information... -update.close=Close -update.viewAllReleases=View All Releases -update.downloadLatest=Download Latest -update.availableUpdates=Available Updates: -update.unableToLoadDetails=Unable to load detailed version information. -update.version=Version +update.urgentUpdateAvailable=🚨 Dostupno ažuriranje +update.updateAvailable=Dostupno ažuriranje +update.modalTitle=Ažuriranje dostupno +update.current=Tekuće +update.latest=Najnovije +update.latestStable=Najnovije stabilno +update.priority=Prioritet +update.recommendedAction=Preporučene akcije +update.breakingChangesDetected=⚠️ Detektovane potencijalno ne-kompatibilne promene +update.breakingChangesMessage=Ažuriranje sadrži nekompatibilne promene. Pogledaj vodič za migraciju ispod. +update.migrationGuides=Vodič za migraciju: +update.viewGuide=Pregledaj vodič +update.loadingDetailedInfo=Učitavam detaljne informacije o verziji... +update.close=Zatvori +update.viewAllReleases=Prikaži sve verzije +update.downloadLatest=Preuzimanje najnovije +update.availableUpdates=Raspoloživa ažuriranja: +update.unableToLoadDetails=Nije moguće učitati detaljne informacije o verziji. +update.version=Verzija # Update priority levels -update.priority.urgent=URGENT -update.priority.normal=NORMAL -update.priority.minor=MINOR -update.priority.low=LOW +update.priority.urgent=HITNO +update.priority.normal=NORMALNO +update.priority.minor=MINORNO +update.priority.low=NISKO # Breaking changes text -update.breakingChanges=Breaking Changes: -update.breakingChangesDefault=This version contains breaking changes -update.migrationGuide=Migration Guide +update.breakingChanges=Ne-kompatibilne promene: +update.breakingChangesDefault=Ova verzija sadrži ne-kompatibilne promene +update.migrationGuide=Vodič za migraciju settings.appVersion=Verzija aplikacije: -settings.downloadOption.title=Odaberi opciju preuzimanja (Za preuzimanje pojedinačnih fajlova bez zip formata): +settings.downloadOption.title=Odaberi opciju preuzimanja (Za preuzimanje pojedinačnih datoteka bez zip formata): settings.downloadOption.1=Otvori u istom prozoru settings.downloadOption.2=Otvori u novom prozoru settings.downloadOption.3=Preuzmi datoteku @@ -491,7 +493,7 @@ teamExists=Tim sa tim imenom već postoji teamNameExists=Drugi tim sa tim imenom već postoji teamNotFound=Tim nije pronađen teamDeleted=Tim obrisan -teamHasUsers=Nije moguće obrisati tim kom su dodeljni korisnici +teamHasUsers=Nije moguće obrisati tim kom su dodeljeni korisnici teamRenamed=Tim uspešno preimenovan # Team user management @@ -533,7 +535,7 @@ endpointStatistics.endpoint=Krajnja tačka endpointStatistics.visits=Poseta endpointStatistics.percentage=Procenat endpointStatistics.loading=Učitavam... -endpointStatistics.failedToLoad=Neuspešno učitavanje podataka o krajnjoj tačci. Pokušaj da osvežiš. +endpointStatistics.failedToLoad=Neuspešno učitavanje podataka o krajnjoj tački. Pokušaj da osvežiš. endpointStatistics.home=Početna strana endpointStatistics.login=Prijava endpointStatistics.top=Top @@ -551,7 +553,7 @@ database.importBackupFile=Uvezi rezervnu kopiju database.createBackupFile=Kreiraj rezervnu kopiju database.downloadBackupFile=Preuzmi rezervnu kopiju database.info_1=Prilikom uvoza podataka, od suštinskog je značaja obezbediti ispravnu strukturu. Ako nisi siguran u ono što radiš, potraži savet i podršku stručnog lica. Greška u strukturi može izazvati neispravno funkcionisanje aplikacije, pa čak i potpunu nemogućnost njenog pokretanja. -database.info_2=Naziv fajla prilikom otpremanja nije bitan. Fajl će kasnije biti preimenovan u format backup_user_yyyyMMddHHmm.sql, kako bi se obezbedila dosledna konvencija imenovanja. +database.info_2=Naziv datoteke prilikom otpremanja nije bitan. Datoteka će kasnije biti preimenovana u format backup_user_yyyyMMddHHmm.sql, kako bi se obezbedila dosledna konvencija imenovanja. database.submit=Uvezi rezervnu kopiju database.importIntoDatabaseSuccessed=Uvoz u bazu uspešan database.backupCreated=Rezervna kopija baze podataka je uspešna napravljena @@ -566,7 +568,7 @@ session.refreshPage=Osveži stranicu ############# # HOME-PAGE # ############# -home.desc=Lokalno hostovano rešenje koje za sve tvoje PDF potrebe na jednom mestu. +home.desc=Lokalno hostovano rešenje za sve tvoje PDF potrebe na jednom mestu. home.searchBar=Pretraži funkcije... @@ -604,6 +606,22 @@ home.imageToPdf.title=Slika u PDF home.imageToPdf.desc=Konvertovanje slika (PNG, JPEG, GIF) u PDF imageToPdf.tags=konverzija,img,jpg,slika,foto +home.cbzToPdf.title=CBZ to PDF +home.cbzToPdf.desc=Convert CBZ comic book archives to PDF format. +cbzToPdf.tags=conversion,comic,book,archive,cbz,zip + +home.cbrToPdf.title=CBR to PDF +home.cbrToPdf.desc=Convert CBR comic book archives to PDF format. +cbrToPdf.tags=conversion,comic,book,archive,cbr,rar + +home.pdfToCbz.title=PDF to CBZ +home.pdfToCbz.desc=Convert PDF files to CBZ comic book archives. +pdfToCbz.tags=conversion,comic,book,archive,cbz,pdf + +home.pdfToCbr.title=PDF to CBR +home.pdfToCbr.desc=Convert PDF files to CBR comic book archives. +pdfToCbr.tags=conversion,comic,book,archive,cbr,rar + home.pdfToImage.title=PDF u Sliku home.pdfToImage.desc=Konvertovanje PDF u sliku (PNG, JPEG, GIF) pdfToImage.tags=konverzija,img,jpg,slika,foto @@ -732,7 +750,7 @@ home.pageLayout.desc=Spajanje više stranica PDF dokumenta u jednu stranicu pageLayout.tags=spajanje,kompozit,pojedinačan-prikaz,organizacija home.scalePages.title=Podesi veličinu/razmeru stranice -home.scalePages.desc=Promena veličine/rezmere stranice i/ili njenog sadržaja +home.scalePages.desc=Promena veličine/razmere stranice i/ili njenog sadržaja scalePages.tags=izmena,modifikacija,dimenzija,adaptacija home.pipeline.title=Tok rada @@ -779,14 +797,14 @@ EMLToPDF.tags=email,konverzija,eml,poruka,transformacija,konvertovanje,elektrons EMLToPDF.title=Email u PDF EMLToPDF.header=Email u PDF EMLToPDF.submit=Konvertuj -EMLToPDF.downloadHtml=Preuzmi HTML međufajl umesto PDF-a +EMLToPDF.downloadHtml=Preuzmi HTML među-datoteku umesto PDF-a EMLToPDF.downloadHtmlHelp=Ovo omogućava da se vidi HTML verzija pre konverzije u PDF i može pomoći u otklanjanju problema sa formatiranjem. EMLToPDF.includeAttachments=Uključi dodatke u PDF EMLToPDF.maxAttachmentSize=Maksimalna veličina dodatka (MB) EMLToPDF.help=Konvertovanje email (EML) datoteka u PDF format uključujući zaglavlje, telo poruke i ugrađene slike EMLToPDF.troubleshootingTip1=Konverzija emaila u HTML je pouzdaniji proces, te se kod serijske obrade preporučuje čuvanje oba formata. EMLToPDF.troubleshootingTip2=Kod malog broja emailova, ako je PDF neispravan, moguće je preuzeti HTML i ispraviti deo problematičnog HTML/CSS koda. -EMLToPDF.troubleshootingTip3=Ugradnja sadržaja, međutim, ne funkcioniše sa HTML fajlovima. +EMLToPDF.troubleshootingTip3=Ugradnja sadržaja, međutim, ne funkcioniše sa HTML datotekama. home.MarkdownToPDF.title=Markdown u PDF home.MarkdownToPDF.desc=Konvertovanje Markdown datoteka u PDF @@ -842,7 +860,7 @@ home.split-by-sections.desc=Deljenje svake stranice PDF-a na manje horizontalne split-by-sections.tags=Deljenje sekcija,Deljenje,Podešavanje home.AddStampRequest.title=Dodaj pečat u PDF -home.AddStampRequest.desc=Dodavanje teksta ili slike pečeta na željenim lokacijama +home.AddStampRequest.desc=Dodavanje teksta ili slike pečata na željenim lokacijama AddStampRequest.tags=Pečat, Dodaj sliku, centriraj sliku, Vodeni žig, PDF, Uključi, Prilagodi @@ -876,6 +894,12 @@ replace-color.selectText.8=Žuti tekst na crnoj pozadini replace-color.selectText.9=Zeleni tekst na crnoj pozadini replace-color.selectText.10=Izaberi boju teksta: replace-color.selectText.11=Izaberi boju pozadine: +replace-color.selectText.12=Colour Space Conversion (CMYK for Printing) +replace-color.selectText.13=CMYK Colour Space Conversion +replace-color.selectText.14=This option converts the PDF from RGB colour space to CMYK colour space, which is optimized for professional printing. This process: +replace-color.selectText.15=Converts colours to CMYK (Cyan, Magenta, Yellow, Black) colour model used by professional printers +replace-color.selectText.16=Optimizes the PDF for print production with prepress settings +replace-color.selectText.17=May result in slight colour changes as CMYK has a smaller colour gamut than RGB replace-color.submit=Zameni @@ -908,13 +932,13 @@ login.alreadyLoggedIn=Već si prijavljen na login.alreadyLoggedIn2=uređaja. Odjavi se sa uređaja i pokušaj ponovo. login.toManySessions=Imaš previše aktivnih sesija login.logoutMessage=Odjavljen si. -login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator. +login.invalidInResponseTo=SAML odgovor je nevažeći ili je istekao. Obratite se administratoru. #auto-redact autoRedact.title=Automatsko cenzurisanje -autoRedact.header=Automatko cenzurisanje +autoRedact.header=Automatsko cenzurisanje autoRedact.colorLabel=Boja: -autoRedact.textsToRedactLabel=Pojmovi za centurisanje (svaki u novi red ): +autoRedact.textsToRedactLabel=Pojmovi za cenzurisanje (svaki u novi red): autoRedact.textsToRedactPlaceholder=npr. \nPoverljivo \nVrhunski tajno autoRedact.useRegexLabel=Koristi regex autoRedact.wholeWordSearchLabel=Pretraga celih reči @@ -1043,9 +1067,9 @@ HTMLToPDF.screen=Ekran #AddStampRequest AddStampRequest.header=Pečatiraj PDF AddStampRequest.title=Dodavanje pečata u PDF -AddStampRequest.stampType=Tip pečeta: +AddStampRequest.stampType=Tip pečata: AddStampRequest.stampText=Tekst pečata: -AddStampRequest.stampImage=Slika pečeta: +AddStampRequest.stampImage=Slika pečata: AddStampRequest.alphabet=Pismo: AddStampRequest.fontSize=Veličina fonta/slike: AddStampRequest.rotation=Rotacija: @@ -1060,9 +1084,9 @@ AddStampRequest.submit=Pošalji #sanitizePDF sanitizePDF.title=Sanitizacija PDF-a -sanitizePDF.header=Sanitizacija PDF fajla +sanitizePDF.header=Sanitizacija PDF dokumenta sanitizePDF.selectText.1=Ukloni JavaScript akcije -sanitizePDF.selectText.2=Ukloni ugrađene fajlove +sanitizePDF.selectText.2=Ukloni ugrađene datoteke sanitizePDF.selectText.3=Ukloni XMP metapodatke sanitizePDF.selectText.4=Ukloni linkove sanitizePDF.selectText.5=Ukloni fontove @@ -1073,7 +1097,7 @@ sanitizePDF.submit=Sanitizuj PDF #addPageNumbers addPageNumbers.title=Numerisanje stranica addPageNumbers.header=Dodavanje brojeva stranica -addPageNumbers.selectText.1=Izaberi PDF fajl: +addPageNumbers.selectText.1=Izaberi PDF dokument: addPageNumbers.selectText.2=Veličina margine: addPageNumbers.selectText.3=Pozicija: addPageNumbers.selectText.4=Početni broj: @@ -1081,7 +1105,7 @@ addPageNumbers.selectText.5=Stranice za numerisanje: addPageNumbers.selectText.6=Prilagođeni tekst: addPageNumbers.customTextDesc=Prilagođeni tekst addPageNumbers.numberPagesDesc=Koje stranice brojati, podrazumevano 'sve', takođe prihvata 1-5 ili 2,5,9 itd. -addPageNumbers.customNumberDesc=Podrazumevano je {n}, takođe prihvata 'Stranica {n} od {ukupno}', 'Tekst-{n}', '{ime_fajla}-{n}' +addPageNumbers.customNumberDesc=Podrazumevano je {n}, takođe prihvata 'Stranica {n} od {total}', 'Tekst-{n}', '{filename}-{n}' addPageNumbers.submit=Numeriši @@ -1112,7 +1136,7 @@ autoSplitPDF.header=Automatsko razdvajanje PDF-a autoSplitPDF.description=Odštampaj, ubaci, skeniraj, otpremi i prepusti nama automatsko razdvajanje dokumenata. Nije potrebno ručno sortiranje. autoSplitPDF.selectText.1=Odštampaj neki od razdelnika sa liste ispod (Crno-belo je u redu). autoSplitPDF.selectText.2=Skeniraj sve dokumente odjednom, ubacivanjem lista razdelnika između njih. -autoSplitPDF.selectText.3=Otpremi jedan veliki skenirani PDF fajl i dozvoli Stirling PDF-u da obavi ostalo. +autoSplitPDF.selectText.3=Otpremi jedan veliki skenirani PDF dokument i dozvoli Stirling PDF-u da obavi ostalo. autoSplitPDF.selectText.4=Listovi razdelnici se automatski detektuju i uklanjaju, obezbeđujući uredan konačni dokument. autoSplitPDF.formPrompt=Pošalji PDF koji sadrži Stirling-PDF listove razdelnike stranica: autoSplitPDF.duplexMode=Dupleks režim (skeniranje prednje i zadnje strane) @@ -1149,7 +1173,7 @@ certSign.jksNote=Napomena: Ako tvoj tip sertifikata nije naveden ispod, konvertu certSign.selectKey=Izaberi svoj privatni ključ (PKCS#8 format, može biti .pem ili .der): certSign.selectCert=Izaberi svoj sertifikat (X.509 format, može biti .pem ili .der): certSign.selectP12=Izaberi svoju PKCS#12 keystore datoteku (.p12 ili .pfx) (Opciono, ako je dostupan, trebalo bi da sadrži tvoj privatni ključ i sertifikat): -certSign.selectJKS=Izaberi svoju Java keystore datoteku (.jks or .keystore): +certSign.selectJKS=Izaberi svoju Java keystore datoteku (.jks ili .keystore): certSign.certType=Tip sertifikata: certSign.password=Unesi lozinku keystore datoteke ili privatnog ključa (ako postoji): certSign.showSig=Prikaži potpis @@ -1217,6 +1241,7 @@ sign.previous=Prethodna strana sign.maintainRatio=Uključi/isključi zadržavanje proporcija sign.undo=Poništi sign.redo=Ponovi +sign.colour=Signature Colour #repair repair.title=Popravi @@ -1249,7 +1274,7 @@ ScannerImageSplit.info=Python nije instaliran. Neophodan je za rad. ocr.title=OCR / Čišćenje skeniranih dokumenata ocr.header=Čišćenje skeniranja / OCR (Optičko prepoznavanje karaktera) ocr.selectText.1=Izaberi jezike koji treba da budu detektovani u PDF-u (navedeni su trenutno detektovani jezici): -ocr.selectText.2=Napravi tekstualni fajl koji sadrži OCR tekst zajedno sa OCR PDF-om +ocr.selectText.2=Napravi tekstualnu datoteku koja sadrži OCR tekst zajedno sa OCR PDF-om ocr.selectText.3=Ispravi stranice koje su skenirane pod uglom rotirajući ih na svoje mesto ocr.selectText.4=Očisti stranicu kako bi se smanjila mogućnost da OCR prepozna tekst u pozadinskoj buci. (Bez promene izlaz) ocr.selectText.5=Očisti stranicu kako bi se smanjila mogućnost da OCR prepozna tekst u pozadinskoj buci, zadržavajući čišćenje u izlazu. @@ -1339,8 +1364,8 @@ pdfOrganiser.placeholder=(npr. 1,3,2 ili 4-8,2,10-12 ili 2n-1) #multiTool -multiTool.title=Višefunkcionalni PDF alat -multiTool.header=Višefunkcionalni PDF alat +multiTool.title=Multifunkcionalni PDF alat +multiTool.header=Multifunkcionalni PDF alat multiTool.uploadPrompts=Naziv datoteke multiTool.selectAll=Izaberi sve multiTool.deselectAll=Poništi sve @@ -1423,6 +1448,34 @@ imageToPDF.selectText.3=Logika za više datoteka (omogućeno samo ako se radi sa imageToPDF.selectText.4=Spoji u jedan PDF imageToPDF.selectText.5=Konvertuj u odvojene PDF-ove +#cbzToPDF +cbzToPDF.title=CBZ to PDF +cbzToPDF.header=CBZ to PDF +cbzToPDF.submit=Convert to PDF +cbzToPDF.selectText=Select CBZ file +cbzToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBZ +pdfToCBZ.title=PDF to CBZ +pdfToCBZ.header=PDF to CBZ +pdfToCBZ.submit=Convert to CBZ +pdfToCBZ.selectText=Select PDF file +pdfToCBZ.dpi=DPI (Dots Per Inch) + +#cbrToPDF +cbrToPDF.title=CBR to PDF +cbrToPDF.header=CBR to PDF +cbrToPDF.submit=Convert to PDF +cbrToPDF.selectText=Select CBR file +cbrToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBR +pdfToCBR.title=PDF to CBR +pdfToCBR.header=PDF to CBR +pdfToCBR.submit=Convert to CBR +pdfToCBR.selectText=Select PDF file +pdfToCBR.dpi=DPI (Dots Per Inch) +pdfToCBR.dpiHelp=Higher DPI results in better quality but larger file size. #pdfToImage pdfToImage.title=PDF u sliku @@ -1435,10 +1488,11 @@ pdfToImage.colorType=Režim boja: pdfToImage.color=Kolor pdfToImage.grey=Monohromatski pdfToImage.blackwhite=Crno-belo (Može izgubiti detalje!) -pdfToImage.dpi=DPI (The server limit is {0} dpi) +pdfToImage.dpi=DPI (Ograničenje servera je {0} dpi) pdfToImage.submit=Konvertuj pdfToImage.info=Python nije instaliran. Neophodan je za WebP konverziju. pdfToImage.placeholder=(npr. 1,2,8 ili 4,7,12-16 ili 2n-1) +pdfToImage.includeAnnotations=Uključi napomene (komentare, isticanja itd.) #addPassword @@ -1596,7 +1650,7 @@ split-by-size-or-count.submit=Potvrdi #overlay-pdfs -overlay-pdfs.header=Preklapanje PDF fajlova +overlay-pdfs.header=Preklapanje PDF dokumenata overlay-pdfs.baseFile.label=Izaberi osnovnu PDF datoteku overlay-pdfs.overlayFiles.label=Izaberi PDF datoteke za preklapanje: overlay-pdfs.mode.label=Izaberi režim preklapanja: @@ -1653,7 +1707,7 @@ survey.meeting.2=Ovo je prilika da: survey.meeting.3=Dobiješ pomoć oko postavljanja, integracije ili rešavanja problema survey.meeting.4=Pružiš direktne povratne informacije o performansama, specifičnim slučajevima i nedostacima funkcionalnosti survey.meeting.5=Pomozi nam da unapredimo Stirling PDF za praktičnu upotrebu u preduzećima -survey.meeting.6=Ukoliko si zainteresov, možeš zakazati direktni termin sa našim timom. (Samo na engleskom jeziku) +survey.meeting.6=Ukoliko si zainteresovan, možeš zakazati direktni termin sa našim timom. (Samo na engleskom jeziku) survey.meeting.7=Radujemo se što ćemo detaljnije istražiti tvoje slučajeve korišćenja i učiniti Stirling PDF još boljim! survey.meeting.notInterested=Nisi poslovni korisnik i/ili nisi zainteresovan za sastanak? survey.meeting.button=Zakaži sastanak @@ -1684,10 +1738,10 @@ splitByChapters.header=Podeli PDF po poglavljima splitByChapters.bookmarkLevel=Nivo oznake u sadržaju: splitByChapters.includeMetadata=Uključi metapodatke splitByChapters.allowDuplicates=Dozvoli duplikate -splitByChapters.desc.1=Ovaj alat deli PDF fajl na više PDF-ova po osnovu strukture poglavlja. +splitByChapters.desc.1=Ovaj alat deli PDF dokument na više PDF-ova po osnovu strukture poglavlja. splitByChapters.desc.2=Nivo oznake: Izaberite nivo oznaka koji će se koristiti za deljenje (0 za najviši nivo, 1 za drugi nivo, itd.). splitByChapters.desc.3=Uključi metapodatke: Ako je označeno, metapodaci iz originalnog PDF-a biće uključeni u svaki podeljeni PDF. -splitByChapters.desc.4=Dozvoli duplikate: Ako je označeno, omogućava da više oznaka na istoj strani kreira odvojene PDF fajlove. +splitByChapters.desc.4=Dozvoli duplikate: Ako je označeno, omogućava da više oznaka na istoj strani kreira odvojene PDF dokumente. splitByChapters.submit=Podeli PDF #File Chooser @@ -1829,7 +1883,7 @@ cookieBanner.popUp.description.2=Ako to ne želite, klikom na 'Ne, hvala' biće cookieBanner.popUp.acceptAllBtn=U redu cookieBanner.popUp.acceptNecessaryBtn=Ne, hvala cookieBanner.popUp.showPreferencesBtn=Upravljaj podešavanjima -cookieBanner.preferencesModal.title=Centar za podešavanja saglasnoti +cookieBanner.preferencesModal.title=Centar za podešavanja saglasnosti cookieBanner.preferencesModal.acceptAllBtn=Prihvati sve cookieBanner.preferencesModal.acceptNecessaryBtn=Odbij sve cookieBanner.preferencesModal.savePreferencesBtn=Sačuvaj podešavanja @@ -1846,8 +1900,8 @@ cookieBanner.preferencesModal.analytics.title=Analitika cookieBanner.preferencesModal.analytics.description=Ovi kolačići nam pomažu da razumemo kako se naši alati koriste, kako bismo mogli da se fokusiramo na razvoj funkcija koje naša zajednica najviše ceni. Budite sigurni — Stirling PDF ne može i nikada neće pratiti sadržaj dokumenata sa kojima radite. #scannerEffect -scannerEffect.title=Lažno skeniranje -scannerEffect.header=Lažno skeniranje +scannerEffect.title=Efekat skenera +scannerEffect.header=Efekat skenera scannerEffect.description=Kreiraj PDF koji izgleda kao da je skeniran scannerEffect.selectPDF=Izaberi PDF: scannerEffect.quality=Kvalitet skeniranja: @@ -1855,19 +1909,19 @@ scannerEffect.quality.low=Nizak scannerEffect.quality.medium=Srednji scannerEffect.quality.high=Visok scannerEffect.rotation=Ugao rotiranja: -scannerEffect.rotation.none=Nijedno +scannerEffect.rotation.none=Nijedan scannerEffect.rotation.slight=Blago scannerEffect.rotation.moderate=Umereno scannerEffect.rotation.severe=Značajno -scannerEffect.submit=Kreiraj lažno skeniranje +scannerEffect.submit=Kreiraj efekat skeniranja #home.scannerEffect -home.scannerEffect.title=Lažno skeniranje +home.scannerEffect.title=Efekat skenera home.scannerEffect.desc=Kreiraj PDF koji izgleda kao da je skeniran scannerEffect.tags=sken,simuliraj,realistično,konvertuj # ScannerEffect advanced settings (frontend) -scannerEffect.advancedSettings=Omogući naprednja podešavanja za skeniranje +scannerEffect.advancedSettings=Omogući napredna podešavanja za skeniranje scannerEffect.colorspace=Režim boja: scannerEffect.colorspace.grayscale=Monohromatski scannerEffect.colorspace.color=Kolor @@ -1877,7 +1931,7 @@ scannerEffect.rotateVariance=Varijacija rotacije (stepeni) scannerEffect.brightness=Osvetljenje scannerEffect.contrast=Kontrast scannerEffect.blur=Zamućenje -scannerEffect.noise=Buka +scannerEffect.noise=Šum scannerEffect.yellowish=Žutilo (simulacija starog papira) scannerEffect.resolution=Rezolucija (DPI) @@ -1893,12 +1947,12 @@ editTableOfContents.replaceExisting=Zameni postojeće obeleživače (isključi d editTableOfContents.editorTitle=Editor obeleživača editTableOfContents.editorDesc=Dodaj i rasporedi obeleživače ispod. Klikni + za dodavanje podređenih obeleživača. editTableOfContents.addBookmark=Dodaj novi obeleživač -editTableOfContents.importBookmarksDefault=Import -editTableOfContents.importBookmarksFromJsonFile=Upload JSON file -editTableOfContents.importBookmarksFromClipboard=Paste from clipboard -editTableOfContents.exportBookmarksDefault=Export -editTableOfContents.exportBookmarksAsJson=Download as JSON -editTableOfContents.exportBookmarksAsText=Copy as text +editTableOfContents.importBookmarksDefault=Uvezi +editTableOfContents.importBookmarksFromJsonFile=Otpremi JSON datoteku +editTableOfContents.importBookmarksFromClipboard=Nalepi +editTableOfContents.exportBookmarksDefault=Izvezi +editTableOfContents.exportBookmarksAsJson=Preuzmi kao JSON +editTableOfContents.exportBookmarksAsText=Kopiraj kao tekst editTableOfContents.desc.1=Ovaj alat omogućava dodavanje ili izmenu sadržaja (obeleživača) u PDF dokumentu. editTableOfContents.desc.2=Moguće je kreirati hijerarhijsku strukturu dodavanjem podređenih obeleživača nadređenim obeleživačima. editTableOfContents.desc.3=Svaki obeleživač zahteva naslov i broj ciljne strane. diff --git a/app/core/src/main/resources/messages_sv_SE.properties b/app/core/src/main/resources/messages_sv_SE.properties index 0182c8f98..d08ee5f1c 100644 --- a/app/core/src/main/resources/messages_sv_SE.properties +++ b/app/core/src/main/resources/messages_sv_SE.properties @@ -137,6 +137,7 @@ lang.yor=Yoruba addPageNumbers.fontSize=Teckenstorlek addPageNumbers.fontName=Typsnitt +addPageNumbers.fontColor=Font Colour pdfPrompt=Välj PDF(er) multiPdfPrompt=Välj PDF-filer (2+) multiPdfDropPrompt=Välj (eller dra och släpp) alla PDF-filer du behöver @@ -193,6 +194,7 @@ error.fileFormatRequired=File must be in {0} format error.invalidFormat=Invalid {0} format: {1} error.endpointDisabled=This endpoint has been disabled by the admin error.urlNotReachable=URL is not reachable, please provide a valid URL +error.invalidUrlFormat=Invalid URL format provided. The provided format is invalid. # DPI and image rendering messages - used by frontend for dynamic translation # Backend sends: [TRANSLATE:messageKey:arg1,arg2] English message @@ -604,6 +606,22 @@ home.imageToPdf.title=Bild till PDF home.imageToPdf.desc=Konvertera en bild (PNG, JPEG, GIF) till PDF. imageToPdf.tags=konvertering,img,jpg,bild,foto +home.cbzToPdf.title=CBZ to PDF +home.cbzToPdf.desc=Convert CBZ comic book archives to PDF format. +cbzToPdf.tags=conversion,comic,book,archive,cbz,zip + +home.cbrToPdf.title=CBR to PDF +home.cbrToPdf.desc=Convert CBR comic book archives to PDF format. +cbrToPdf.tags=conversion,comic,book,archive,cbr,rar + +home.pdfToCbz.title=PDF to CBZ +home.pdfToCbz.desc=Convert PDF files to CBZ comic book archives. +pdfToCbz.tags=conversion,comic,book,archive,cbz,pdf + +home.pdfToCbr.title=PDF to CBR +home.pdfToCbr.desc=Convert PDF files to CBR comic book archives. +pdfToCbr.tags=conversion,comic,book,archive,cbr,rar + home.pdfToImage.title=PDF till bild home.pdfToImage.desc=Konvertera en PDF till en bild. (PNG, JPEG, GIF) pdfToImage.tags=konvertering,img,jpg,bild,foto @@ -876,6 +894,12 @@ replace-color.selectText.8=Gul text på svart bakgrund replace-color.selectText.9=Grön text på svart bakgrund replace-color.selectText.10=Välj textfärg replace-color.selectText.11=Välj bakgrundsfärg +replace-color.selectText.12=Colour Space Conversion (CMYK for Printing) +replace-color.selectText.13=CMYK Colour Space Conversion +replace-color.selectText.14=This option converts the PDF from RGB colour space to CMYK colour space, which is optimized for professional printing. This process: +replace-color.selectText.15=Converts colours to CMYK (Cyan, Magenta, Yellow, Black) colour model used by professional printers +replace-color.selectText.16=Optimizes the PDF for print production with prepress settings +replace-color.selectText.17=May result in slight colour changes as CMYK has a smaller colour gamut than RGB replace-color.submit=Ersätt @@ -1081,7 +1105,7 @@ addPageNumbers.selectText.5=Sidor att numrera addPageNumbers.selectText.6=Anpassad text addPageNumbers.customTextDesc=Anpassad text addPageNumbers.numberPagesDesc=Vilka sidor som ska numreras, standard 'all', accepterar även 1-5 eller 2,5,9 etc -addPageNumbers.customNumberDesc=Standard är {n}, accepterar även 'Sida {n} av {total}', 'Text-{n}', '{filnamn}-{n} +addPageNumbers.customNumberDesc=Standard är {n}, accepterar även 'Sida {n} av {total}', 'Text-{n}', '{filename}-{n}' addPageNumbers.submit=Lägg till sidnummer @@ -1217,6 +1241,7 @@ sign.previous=Föregående sida sign.maintainRatio=Toggle maintain aspect ratio sign.undo=Undo sign.redo=Redo +sign.colour=Signature Colour #repair repair.title=Reparera @@ -1423,6 +1448,34 @@ imageToPDF.selectText.3=Multifillogik (Endast aktiverad om man arbetar med flera imageToPDF.selectText.4=Slå samman till en enda PDF imageToPDF.selectText.5=Konvertera till separata PDF-filer +#cbzToPDF +cbzToPDF.title=CBZ to PDF +cbzToPDF.header=CBZ to PDF +cbzToPDF.submit=Convert to PDF +cbzToPDF.selectText=Select CBZ file +cbzToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBZ +pdfToCBZ.title=PDF to CBZ +pdfToCBZ.header=PDF to CBZ +pdfToCBZ.submit=Convert to CBZ +pdfToCBZ.selectText=Select PDF file +pdfToCBZ.dpi=DPI (Dots Per Inch) + +#cbrToPDF +cbrToPDF.title=CBR to PDF +cbrToPDF.header=CBR to PDF +cbrToPDF.submit=Convert to PDF +cbrToPDF.selectText=Select CBR file +cbrToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBR +pdfToCBR.title=PDF to CBR +pdfToCBR.header=PDF to CBR +pdfToCBR.submit=Convert to CBR +pdfToCBR.selectText=Select PDF file +pdfToCBR.dpi=DPI (Dots Per Inch) +pdfToCBR.dpiHelp=Higher DPI results in better quality but larger file size. #pdfToImage pdfToImage.title=PDF till bild @@ -1439,6 +1492,7 @@ pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Konvertera pdfToImage.info=Python är inte installerat. Krävs för WebP-konvertering. pdfToImage.placeholder=(t.ex. 1,2,8 eller 4,7,12-16 eller 2n-1) +pdfToImage.includeAnnotations=Include annotations (comments, highlights etc.) #addPassword diff --git a/app/core/src/main/resources/messages_th_TH.properties b/app/core/src/main/resources/messages_th_TH.properties index a0473bdef..7a8510998 100644 --- a/app/core/src/main/resources/messages_th_TH.properties +++ b/app/core/src/main/resources/messages_th_TH.properties @@ -137,6 +137,7 @@ lang.yor=Yoruba addPageNumbers.fontSize=ขนาดตัวอักษร addPageNumbers.fontName=ชื่อฟอนต์ +addPageNumbers.fontColor=Font Colour pdfPrompt=เลือก PDF multiPdfPrompt=เลือก PDF หลายไฟล์ (2 ขึ้นไป) multiPdfDropPrompt=เลือก (หรือลากและวาง) PDF ทั้งหมดที่คุณต้องการ @@ -193,6 +194,7 @@ error.fileFormatRequired=File must be in {0} format error.invalidFormat=Invalid {0} format: {1} error.endpointDisabled=This endpoint has been disabled by the admin error.urlNotReachable=URL is not reachable, please provide a valid URL +error.invalidUrlFormat=Invalid URL format provided. The provided format is invalid. # DPI and image rendering messages - used by frontend for dynamic translation # Backend sends: [TRANSLATE:messageKey:arg1,arg2] English message @@ -604,6 +606,22 @@ home.imageToPdf.title=รูปภาพเป็น PDF home.imageToPdf.desc=แปลงรูปภาพ (PNG, JPEG, GIF) เป็น PDF imageToPdf.tags=การแปลง, รูปภาพ, JPG, ภาพ, รูปถ่าย +home.cbzToPdf.title=CBZ to PDF +home.cbzToPdf.desc=Convert CBZ comic book archives to PDF format. +cbzToPdf.tags=conversion,comic,book,archive,cbz,zip + +home.cbrToPdf.title=CBR to PDF +home.cbrToPdf.desc=Convert CBR comic book archives to PDF format. +cbrToPdf.tags=conversion,comic,book,archive,cbr,rar + +home.pdfToCbz.title=PDF to CBZ +home.pdfToCbz.desc=Convert PDF files to CBZ comic book archives. +pdfToCbz.tags=conversion,comic,book,archive,cbz,pdf + +home.pdfToCbr.title=PDF to CBR +home.pdfToCbr.desc=Convert PDF files to CBR comic book archives. +pdfToCbr.tags=conversion,comic,book,archive,cbr,rar + home.pdfToImage.title=PDF เป็นรูปภาพ home.pdfToImage.desc=แปลง PDF เป็นรูปภาพ (PNG, JPEG, GIF) pdfToImage.tags=การแปลง, รูปภาพ, JPG, ภาพ, รูปถ่าย @@ -876,6 +894,12 @@ replace-color.selectText.8=Yellow text on black background replace-color.selectText.9=Green text on black background replace-color.selectText.10=Choose text Color replace-color.selectText.11=Choose background Color +replace-color.selectText.12=Colour Space Conversion (CMYK for Printing) +replace-color.selectText.13=CMYK Colour Space Conversion +replace-color.selectText.14=This option converts the PDF from RGB colour space to CMYK colour space, which is optimized for professional printing. This process: +replace-color.selectText.15=Converts colours to CMYK (Cyan, Magenta, Yellow, Black) colour model used by professional printers +replace-color.selectText.16=Optimizes the PDF for print production with prepress settings +replace-color.selectText.17=May result in slight colour changes as CMYK has a smaller colour gamut than RGB replace-color.submit=Replace @@ -1217,6 +1241,7 @@ sign.previous=Previous page sign.maintainRatio=Toggle maintain aspect ratio sign.undo=Undo sign.redo=Redo +sign.colour=Signature Colour #repair repair.title=ซ่อมแซม @@ -1423,6 +1448,34 @@ imageToPDF.selectText.3=ตรรกะหลายไฟล์ (เปิดใ imageToPDF.selectText.4=รวมเป็น PDF เดียว imageToPDF.selectText.5=แปลงเป็น PDF แยก +#cbzToPDF +cbzToPDF.title=CBZ to PDF +cbzToPDF.header=CBZ to PDF +cbzToPDF.submit=Convert to PDF +cbzToPDF.selectText=Select CBZ file +cbzToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBZ +pdfToCBZ.title=PDF to CBZ +pdfToCBZ.header=PDF to CBZ +pdfToCBZ.submit=Convert to CBZ +pdfToCBZ.selectText=Select PDF file +pdfToCBZ.dpi=DPI (Dots Per Inch) + +#cbrToPDF +cbrToPDF.title=CBR to PDF +cbrToPDF.header=CBR to PDF +cbrToPDF.submit=Convert to PDF +cbrToPDF.selectText=Select CBR file +cbrToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBR +pdfToCBR.title=PDF to CBR +pdfToCBR.header=PDF to CBR +pdfToCBR.submit=Convert to CBR +pdfToCBR.selectText=Select PDF file +pdfToCBR.dpi=DPI (Dots Per Inch) +pdfToCBR.dpiHelp=Higher DPI results in better quality but larger file size. #pdfToImage pdfToImage.title=PDF เป็นรูปภาพ @@ -1439,6 +1492,7 @@ pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=แปลง pdfToImage.info=Python ไม่มีการติดตั้ง จำเป็นสำหรับการแปลง WebP pdfToImage.placeholder=(เช่น 1,2,8 หรือ 4,7,12-16 หรือ 2n-1) +pdfToImage.includeAnnotations=Include annotations (comments, highlights etc.) #addPassword diff --git a/app/core/src/main/resources/messages_tr_TR.properties b/app/core/src/main/resources/messages_tr_TR.properties index 155b4365d..c9c5ccb74 100644 --- a/app/core/src/main/resources/messages_tr_TR.properties +++ b/app/core/src/main/resources/messages_tr_TR.properties @@ -5,138 +5,139 @@ language.direction=ltr # Language names for reuse throughout the application -lang.afr=Afrikaans -lang.amh=Amharic -lang.ara=Arabic -lang.asm=Assamese -lang.aze=Azerbaijani -lang.aze_cyrl=Azerbaijani (Cyrillic) -lang.bel=Belarusian -lang.ben=Bengali -lang.bod=Tibetan -lang.bos=Bosnian -lang.bre=Breton -lang.bul=Bulgarian -lang.cat=Catalan +lang.afr=Afrikaanca +lang.amh=Amharca +lang.ara=Arapça +lang.asm=Assamca +lang.aze=Azerice +lang.aze_cyrl=Azerice (Kiril) +lang.bel=Beyaz Rusça (Belarusça) +lang.ben=Bengalce +lang.bod=Tibetçe +lang.bos=Boşnakça +lang.bre=Bretonca +lang.bul=Bulgarca +lang.cat=Katalanca lang.ceb=Cebuano -lang.ces=Czech -lang.chi_sim=Chinese (Simplified) -lang.chi_sim_vert=Chinese (Simplified, Vertical) -lang.chi_tra=Chinese (Traditional) -lang.chi_tra_vert=Chinese (Traditional, Vertical) -lang.chr=Cherokee -lang.cos=Corsican -lang.cym=Welsh -lang.dan=Danish -lang.dan_frak=Danish (Fraktur) -lang.deu=German -lang.deu_frak=German (Fraktur) -lang.div=Divehi +lang.ces=Çekçe +lang.chi_sim=Çince (Basitleştirilmiş) +lang.chi_sim_vert=Çince (Basitleştirilmiş, Dikey) +lang.chi_tra=Çince (Geleneksel) +lang.chi_tra_vert=Çince (Geleneksel, Dikey) +lang.chr=Çerokice +lang.cos=Korsikaca +lang.cym=Gallerce (Galce) +lang.dan=Danca +lang.dan_frak=Danca (Fraktur) +lang.deu=Almanca +lang.deu_frak=Almanca (Fraktur) +lang.div=Maldivce (Divehi) lang.dzo=Dzongkha -lang.ell=Greek -lang.eng=English -lang.enm=English, Middle (1100-1500) +lang.ell=Yunanca +lang.eng=İngilizce +lang.enm=İngilizce, Orta Çağ (1100-1500) lang.epo=Esperanto -lang.equ=Math / equation detection module -lang.est=Estonian -lang.eus=Basque -lang.fao=Faroese -lang.fas=Persian -lang.fil=Filipino -lang.fin=Finnish -lang.fra=French -lang.frk=Frankish -lang.frm=French, Middle (ca.1400-1600) -lang.fry=Western Frisian -lang.gla=Scottish Gaelic -lang.gle=Irish -lang.glg=Galician -lang.grc=Ancient Greek -lang.guj=Gujarati -lang.hat=Haitian, Haitian Creole -lang.heb=Hebrew -lang.hin=Hindi -lang.hrv=Croatian -lang.hun=Hungarian -lang.hye=Armenian -lang.iku=Inuktitut -lang.ind=Indonesian -lang.isl=Icelandic -lang.ita=Italian -lang.ita_old=Italian (Old) -lang.jav=Javanese -lang.jpn=Japanese -lang.jpn_vert=Japanese (Vertical) +lang.equ=Matematik / denklem tanıma modülü +lang.est=Estonca +lang.eus=Baskça +lang.fao=Faroece +lang.fas=Farsça +lang.fil=Filipince +lang.fin=Fince +lang.fra=Fransızca +lang.frk=Frankça +lang.frm=Fransızca, Orta Çağ (yaklaşık 1400-1600) +lang.fry=Batı Frizce +lang.gla=İskoç Galcesi +lang.gle=İrlandaca +lang.glg=Galiçyaca +lang.grc=Antik Yunanca +lang.guj=Gujaratça +lang.hat=Haiti Creole +lang.heb=İbranice +lang.hin=Hintçe +lang.hrv=Hırvatça +lang.hun=Macarca +lang.hye=Ermenice +lang.iku=İnuktitut +lang.ind=Endonezce +lang.isl=İzlandaca +lang.ita=İtalyanca +lang.ita_old=İtalyanca (Eski) +lang.jav=Cava dili +lang.jpn=Japonca +lang.jpn_vert=Japonca (Dikey) lang.kan=Kannada -lang.kat=Georgian -lang.kat_old=Georgian (Old) -lang.kaz=Kazakh -lang.khm=Central Khmer -lang.kir=Kirghiz, Kyrgyz -lang.kmr=Northern Kurdish -lang.kor=Korean -lang.kor_vert=Korean (Vertical) -lang.lao=Lao -lang.lat=Latin -lang.lav=Latvian -lang.lit=Lithuanian -lang.ltz=Luxembourgish -lang.mal=Malayalam +lang.kat=Gürcüce +lang.kat_old=Gürcüce (Eski) +lang.kaz=Kazakça +lang.khm=Merkez Khmer dili +lang.kir=Kırgızca +lang.kmr=Kuzey Kürtçesi +lang.kor=Korece +lang.kor_vert=Korece (Dikey) +lang.lao=Laosça +lang.lat=Latince +lang.lav=Letonca +lang.lit=Litvanca +lang.ltz=Lüksemburgca +lang.mal=Malayalamca lang.mar=Marathi -lang.mkd=Macedonian -lang.mlt=Maltese -lang.mon=Mongolian -lang.mri=Maori -lang.msa=Malay -lang.mya=Burmese -lang.nep=Nepali -lang.nld=Dutch; Flemish -lang.nor=Norwegian -lang.oci=Occitan (post 1500) +lang.mkd=Makedonca +lang.mlt=Maltaca +lang.mon=Moğolca +lang.mri=Maorice +lang.msa=Malayca +lang.mya=Birmanca (Burma) +lang.nep=Nepalce +lang.nld=Hollandaca; Flamanca +lang.nor=Norveççe +lang.oci=Oksitanca (1500 sonrası) lang.ori=Oriya -lang.osd=Orientation and script detection module -lang.pan=Panjabi, Punjabi -lang.pol=Polish -lang.por=Portuguese -lang.pus=Pushto, Pashto -lang.que=Quechua -lang.ron=Romanian, Moldavian, Moldovan -lang.rus=Russian -lang.san=Sanskrit -lang.sin=Sinhala, Sinhalese -lang.slk=Slovak -lang.slk_frak=Slovak (Fraktur) -lang.slv=Slovenian -lang.snd=Sindhi -lang.spa=Spanish -lang.spa_old=Spanish (Old) -lang.sqi=Albanian -lang.srp=Serbian -lang.srp_latn=Serbian (Latin) -lang.sun=Sundanese -lang.swa=Swahili -lang.swe=Swedish -lang.syr=Syriac -lang.tam=Tamil -lang.tat=Tatar +lang.osd=Yönlendirme ve yazı tipi algılama modülü +lang.pan=Pencapça +lang.pol=Lehçe (Polonyaca) +lang.por=Portekizce +lang.pus=Peştuca +lang.que=Keçuva dili +lang.ron=Rumence, Moldovca +lang.rus=Rusça +lang.san=Sanskritçe +lang.sin=Seylanca (Sinhala) +lang.slk=Slovakça +lang.slk_frak=Slovakça (Fraktur) +lang.slv=Slovence +lang.snd=Sindhice +lang.spa=İspanyolca +lang.spa_old=İspanyolca (Eski) +lang.sqi=Arnavutça +lang.srp=Sırpça +lang.srp_latn=Sırpça (Latin alfabesiyle) +lang.sun=Sundaca +lang.swa=Svahili dili +lang.swe=İsveççe +lang.syr=Süryanice +lang.tam=Tamilce +lang.tat=Tatarca lang.tel=Telugu -lang.tgk=Tajik +lang.tgk=Tacikçe lang.tgl=Tagalog -lang.tha=Thai +lang.tha=Tayca lang.tir=Tigrinya -lang.ton=Tonga (Tonga Islands) -lang.tur=Turkish -lang.uig=Uighur, Uyghur -lang.ukr=Ukrainian -lang.urd=Urdu -lang.uzb=Uzbek -lang.uzb_cyrl=Uzbek (Cyrillic) -lang.vie=Vietnamese -lang.yid=Yiddish +lang.ton=Tonga dili (Tonga Adaları) +lang.tur=Türkçe +lang.uig=Uygurca +lang.ukr=Ukraynaca +lang.urd=Urduca +lang.uzb=Özbekçe +lang.uzb_cyrl=Özbekçe (Kiril) +lang.vie=Vietnamca +lang.yid=Yidiş lang.yor=Yoruba -addPageNumbers.fontSize=Font Büyüklüğü -addPageNumbers.fontName=Font İsmi +addPageNumbers.fontSize=Yazı Tipi Büyüklüğü +addPageNumbers.fontName=Yazı Tipi İsmi +addPageNumbers.fontColor=Yazı Tipi Rengi pdfPrompt=PDF(leri) seçin multiPdfPrompt=PDFleri seçin (2+) multiPdfDropPrompt=Tüm gerekli PDF'leri seçin (ya da sürükleyip bırakın) @@ -146,8 +147,8 @@ uploadLimit=Maksimum dosya boyutu: uploadLimitExceededSingular=çok büyük. İzin verilen maksimum boyut: uploadLimitExceededPlural=çok büyük. İzin verilen maksimum boyut: processTimeWarning=Uyarı: Bu işlem, dosya boyutuna bağlı olarak bir dakikaya kadar sürebilir. -pageOrderPrompt=Özel Sayfa Sırası (Virgülle ayrılmış sayfa numaraları veya 2n+1 gibi bir fonksiyon girin) : -pageSelectionPrompt=Özel Sayfa Seçimi (1,5,6 sayfa numaralarının virgülle ayrılmış bir listesini veya 2n+1 gibi bir fonksiyon girin) : +pageOrderPrompt=Özel Sayfa Sırası (Virgülle ayrılmış sayfa numaraları veya 2n+1 gibi bir fonksiyon girin): +pageSelectionPrompt=Özel Sayfa Seçimi (1,5,6 sayfa numaralarının virgülle ayrılmış bir listesini veya 2n+1 gibi bir fonksiyon girin): goToPage=Sayfaya Git true=Doğru false=Yanlış @@ -170,67 +171,68 @@ sizes.medium=Orta sizes.large=Büyük sizes.x-large=Çok Büyük error.pdfPassword=PDF belgesi şifreli ve şifre ya sağlanmadı ya da yanlış. -error.pdfCorrupted=PDF file appears to be corrupted or damaged. Please try using the 'Repair PDF' feature first to fix the file before proceeding with this operation. -error.pdfCorruptedMultiple=One or more PDF files appear to be corrupted or damaged. Please try using the 'Repair PDF' feature on each file first before attempting to merge them. -error.pdfCorruptedDuring=Error {0}: PDF file appears to be corrupted or damaged. Please try using the 'Repair PDF' feature first to fix the file before proceeding with this operation. +error.pdfCorrupted=PDF dosyası bozuk veya hasarlı görünüyor. Lütfen bu işlemi gerçekleştirmeden önce dosyayı düzeltmek için 'PDF Onar' özelliğini kullanmayı deneyin. +error.pdfCorruptedMultiple=Bir veya daha fazla PDF dosyası bozuk veya hasarlı görünüyor. Lütfen birleştirmeye çalışmadan önce her dosya için 'PDF Onar' özelliğini kullanmayı deneyin. +error.pdfCorruptedDuring=Hata {0}: PDF dosyası bozuk veya hasarlı görünüyor. Lütfen bu işlemi gerçekleştirmeden önce dosyayı düzeltmek için 'PDF Onar' özelliğini kullanmayı deneyin. # Frontend corruption error messages -error.pdfInvalid=The PDF file "{0}" appears to be corrupted or has an invalid structure. Please try using the 'Repair PDF' feature to fix the file before proceeding. -error.tryRepair=Try using the Repair PDF feature to fix corrupted files. +error.pdfInvalid="{0}" adlı PDF dosyası bozuk görünüyor veya geçersiz bir yapıya sahip. Lütfen işlemi gerçekleştirmeden önce dosyayı düzeltmek için 'PDF Onar' özelliğini kullanmayı deneyin. +error.tryRepair=Bozuk dosyaları düzeltmek için PDF Onar özelliğini kullanmayı deneyin. # Additional error messages -error.pdfEncryption=The PDF appears to have corrupted encryption data. This can happen when the PDF was created with incompatible encryption methods. Please try using the 'Repair PDF' feature first, or contact the document creator for a new copy. -error.fileProcessing=An error occurred while processing the file during {0} operation: {1} +error.pdfEncryption=PDF dosyasının şifreleme verileri bozulmuş görünüyor. Bu, PDF uyumsuz şifreleme yöntemleriyle oluşturulduğunda meydana gelebilir. Lütfen önce 'PDF Onar' özelliğini kullanmayı deneyin veya belgenin oluşturucusuyla iletişime geçerek yeni bir kopya isteyin. +error.fileProcessing={0} işlemi sırasında dosya işlenirken bir hata oluştu: {1} # Generic error message templates -error.toolNotInstalled={0} is not installed -error.toolRequired={0} is required for {1} -error.conversionFailed={0} conversion failed -error.commandFailed={0} command failed -error.algorithmNotAvailable={0} algorithm not available -error.optionsNotSpecified={0} options are not specified -error.fileFormatRequired=File must be in {0} format -error.invalidFormat=Invalid {0} format: {1} -error.endpointDisabled=This endpoint has been disabled by the admin -error.urlNotReachable=URL is not reachable, please provide a valid URL +error.toolNotInstalled={0} yüklü değil +error.toolRequired={1} işlemi için {0} gereklidir +error.conversionFailed={0} dönüştürme işlemi başarısız oldu +error.commandFailed={0} komutu başarısız oldu +error.algorithmNotAvailable={0} algoritması kullanılamıyor +error.optionsNotSpecified={0} seçenekleri belirtilmemiş +error.fileFormatRequired=Dosya {0} formatında olmalıdır +error.invalidFormat=Geçersiz {0} formatı: {1} +error.endpointDisabled=Bu uç nokta yönetici tarafından devre dışı bırakılmıştır +error.urlNotReachable=URL erişilebilir değil, lütfen geçerli bir URL sağlayın +error.invalidUrlFormat=Geçersiz URL biçimi girildi. Girilen biçim geçersiz. # DPI and image rendering messages - used by frontend for dynamic translation # Backend sends: [TRANSLATE:messageKey:arg1,arg2] English message # Frontend parses this and replaces with localized versions using these keys -error.dpiExceedsLimit=DPI value {0} exceeds maximum safe limit of {1}. High DPI values can cause memory issues and crashes. Please use a lower DPI value. -error.pageTooBigForDpi=PDF page {0} is too large to render at {1} DPI. Please try a lower DPI value (recommended: 150 or less). -error.pageTooBigExceedsArray=PDF page {0} is too large to render at {1} DPI. The resulting image would exceed Java's maximum array size. Please try a lower DPI value (recommended: 150 or less). -error.pageTooBigFor300Dpi=PDF page {0} is too large to render at 300 DPI. The resulting image would exceed Java's maximum array size. Please use a lower DPI value for PDF-to-image conversion. +error.dpiExceedsLimit=DPI değeri {0}, maksimum güvenli sınır olan {1} değerini aşıyor. Yüksek DPI değerleri bellek sorunlarına ve çökme hatalarına neden olabilir. Lütfen daha düşük bir DPI değeri kullanın. +error.pageTooBigForDpi=PDF sayfası {0}, {1} DPI değerinde işlenemeyecek kadar büyük. Lütfen daha düşük bir DPI değeri deneyin (önerilen: 150 veya daha az). +error.pageTooBigExceedsArray=PDF sayfası {0}, {1} DPI değerinde işlenemeyecek kadar büyük. Ortaya çıkan görüntü Java'nın maksimum dizi boyutunu aşacaktır. Lütfen daha düşük bir DPI değeri deneyin (önerilen: 150 veya daha az). +error.pageTooBigFor300Dpi=PDF sayfası {0}, 300 DPI değerinde işlenemeyecek kadar büyük. Ortaya çıkan görüntü Java'nın maksimum dizi boyutunu aşacaktır. Lütfen PDF'den görüntüye dönüştürme işlemi için daha düşük bir DPI değeri kullanın. # URL and website conversion messages # System requirements messages # Authentication and security messages -error.apiKeyInvalid=API key is not valid. -error.userNotFound=User not found. -error.passwordRequired=Password must not be null. -error.accountLocked=Your account has been locked due to too many failed login attempts. -error.invalidEmail=Invalid email addresses provided. -error.emailAttachmentRequired=An attachment is required to send the email. -error.signatureNotFound=Signature file not found. +error.apiKeyInvalid=API anahtarı geçerli değil. +error.userNotFound=Kullanıcı bulunamadı. +error.passwordRequired=Parola boş bırakılamaz. +error.accountLocked=Çok fazla başarısız giriş denemesi nedeniyle hesabınız kilitlendi. +error.invalidEmail=Geçersiz e-posta adresleri sağlandı. +error.emailAttachmentRequired=E-posta gönderebilmek için bir ek dosya gereklidir. +error.signatureNotFound=İmza dosyası bulunamadı. # File processing messages -error.fileNotFound=File not found with ID: {0} +error.fileNotFound=Dosya bulunamadı. Dosya kimliği: {0} # Database and configuration messages -error.noBackupScripts=No backup scripts were found. -error.unsupportedProvider={0} is not currently supported. -error.pathTraversalDetected=Path traversal detected for security reasons. +error.noBackupScripts=Yedekleme betikleri bulunamadı. +error.unsupportedProvider={0} şu anda desteklenmiyor. +error.pathTraversalDetected=Güvenlik nedeniyle yol geçişi (path traversal) tespit edildi. # Validation messages -error.invalidArgument=Invalid argument: {0} -error.argumentRequired={0} must not be null -error.operationFailed=Operation failed: {0} -error.angleNotMultipleOf90=Angle must be a multiple of 90 -error.pdfBookmarksNotFound=No PDF bookmarks/outline found in document -error.fontLoadingFailed=Error processing font file -error.fontDirectoryReadFailed=Failed to read font directory +error.invalidArgument=Geçersiz argüman: {0} +error.argumentRequired={0} boş olamaz +error.operationFailed=İşlem başarısız oldu: {0} +error.angleNotMultipleOf90=Açı 90'ın katı olmalıdır +error.pdfBookmarksNotFound=Belgede herhangi bir PDF yer imi / içindekiler bulunamadı +error.fontLoadingFailed=Yazı tipi dosyası işlenirken hata oluştu +error.fontDirectoryReadFailed=Yazı tipi dizini okunamadı delete=Sil username=Kullanıcı Adı password=Parola @@ -260,7 +262,7 @@ disabledCurrentUserMessage=Mevcut kullanıcı devre dışı bırakılamaz downgradeCurrentUserLongMessage=Mevcut kullanıcının rolü düşürülemiyor. Bu nedenle, mevcut kullanıcı gösterilmeyecektir. userAlreadyExistsOAuthMessage=Kullanıcı zaten bir OAuth2 kullanıcısı olarak mevcut. userAlreadyExistsWebMessage=Kullanıcı zaten bir web kullanıcısı olarak mevcut. -invalidRoleMessage=Invalid role. +invalidRoleMessage=Geçersiz rol. error=Hata oops=Tüh! help=Yardım @@ -273,7 +275,7 @@ color=Renk sponsor=Bağış info=Bilgi pro=Pro -proFeatures=Pro Features +proFeatures=Pro Özellikler page=Sayfa pages=Sayfalar loading=Yükleniyor... @@ -281,11 +283,11 @@ addToDoc=Dökümana Ekle reset=Sıfırla apply=Uygula noFileSelected=Hiçbir dosya seçilmedi. Lütfen bir dosya yükleyin. -view=View +view=Görüntüle cancel=İptal -back.toSettings=Ayarlar’a Geri Dön -back.toHome=Ana Sayfa’ya Geri Dön +back.toSettings=Ayarlar'a Geri Dön +back.toHome=Ana Sayfa'ya Geri Dön back.toAdmin=Yönetim Paneline Geri Dön legal.privacy=Gizlilik Politikası @@ -327,15 +329,15 @@ enterpriseEdition.button=Pro Sürümüne Yükselt enterpriseEdition.warning=Bu özellik yalnızca Pro kullanıcılarına sunulmaktadır. enterpriseEdition.yamlAdvert=Stirling PDF Pro, YAML yapılandırma dosyalarını ve diğer SSO özelliklerini destekler. enterpriseEdition.ssoAdvert=Daha fazla kullanıcı yönetimi özelliği mi arıyorsunuz? Stirling PDF Pro'ya göz atın -enterpriseEdition.proTeamFeatureDisabled=Team management features require a Pro licence or higher +enterpriseEdition.proTeamFeatureDisabled=Takım yönetimi özellikleri Pro lisansı veya daha üstü gerektirir ################# # Analytics # ################# -analytics.title=Stirling PDF’i daha iyi hale getirmek ister misiniz? +analytics.title=Stirling PDF'i daha iyi hale getirmek ister misiniz? analytics.paragraph1=Stirling PDF, ürünü geliştirmemize yardımcı olmak için isteğe bağlı analizleri içerir. Kişisel bilgileri veya dosya içeriklerini asla takip etmiyoruz. -analytics.paragraph2=Stirling PDF’in büyümesine destek olmak ve kullanıcılarımızı daha iyi anlayabilmemiz için analizleri etkinleştirmeyi düşünebilirsiniz. +analytics.paragraph2=Stirling PDF'in büyümesine destek olmak ve kullanıcılarımızı daha iyi anlayabilmemiz için analizleri etkinleştirmeyi düşünebilirsiniz. analytics.enable=Analizi Etkinleştir analytics.disable=Analizi Devre Dışı Bırak analytics.settings=Analiz ayarlarını config/settings.yml dosyasından değiştirebilirsiniz @@ -345,7 +347,7 @@ analytics.settings=Analiz ayarlarını config/settings.yml dosyasından değişt # NAVBAR # ############# navbar.favorite=Favoriler -navbar.recent=New and recently updated +navbar.recent=Yeni ve son güncellenenler navbar.darkmode=Karanlık Mod navbar.language=Diller navbar.settings=Ayarlar @@ -368,36 +370,36 @@ settings.update=Güncelleme mevcut settings.updateAvailable={0} mevcut kurulu sürümdür. Yeni bir sürüm ({1}) mevcuttur. # Update modal and notification strings -update.urgentUpdateAvailable=🚨 Update Available -update.updateAvailable=Update Available -update.modalTitle=Update Available -update.current=Current -update.latest=Latest -update.latestStable=Latest Stable -update.priority=Priority -update.recommendedAction=Recommended Action -update.breakingChangesDetected=⚠️ Breaking Changes Detected -update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. -update.migrationGuides=Migration Guides: -update.viewGuide=View Guide -update.loadingDetailedInfo=Loading detailed version information... -update.close=Close -update.viewAllReleases=View All Releases -update.downloadLatest=Download Latest -update.availableUpdates=Available Updates: -update.unableToLoadDetails=Unable to load detailed version information. -update.version=Version +update.urgentUpdateAvailable=🚨 Güncelleme Mevcut +update.updateAvailable=Güncelleme Mevcut +update.modalTitle=Güncelleme Mevcut +update.current=Mevcut +update.latest=En Yeni +update.latestStable=En Yeni Kararlı +update.priority=Öncelik +update.recommendedAction=Önerilen İşlem +update.breakingChangesDetected=⚠️ Kırıcı Değişiklikler Tespit Edildi +update.breakingChangesMessage=Bu güncelleme kırıcı değişiklikler içeriyor. Lütfen aşağıdaki geçiş kılavuzlarını inceleyin. +update.migrationGuides=Geçiş Kılavuzları: +update.viewGuide=Kılavuzu Görüntüle +update.loadingDetailedInfo=Ayrıntılı sürüm bilgileri yükleniyor... +update.close=Kapat +update.viewAllReleases=Tüm Sürümleri Görüntüle +update.downloadLatest=En Yeniyi İndir +update.availableUpdates=Mevcut Güncellemeler: +update.unableToLoadDetails=Ayrıntılı sürüm bilgileri yüklenemedi. +update.version=Sürüm # Update priority levels -update.priority.urgent=URGENT +update.priority.urgent=ACİL update.priority.normal=NORMAL -update.priority.minor=MINOR -update.priority.low=LOW +update.priority.minor=ÖNEMSİZ +update.priority.low=DÜŞÜK # Breaking changes text -update.breakingChanges=Breaking Changes: -update.breakingChangesDefault=This version contains breaking changes -update.migrationGuide=Migration Guide +update.breakingChanges=Kırıcı Değişiklikler: +update.breakingChangesDefault=Bu sürüm kırıcı değişiklikler içeriyor +update.migrationGuide=Geçiş Kılavuzu settings.appVersion=Uygulama Sürümü: settings.downloadOption.title=İndirme seçeneği seçin (Zip olmayan tek dosya indirmeler için): settings.downloadOption.1=Aynı pencerede aç @@ -472,18 +474,18 @@ adminUserSettings.disabledUsers=Devre Dışı Kullanıcılar: adminUserSettings.totalUsers=Toplam Kullanıcılar: adminUserSettings.lastRequest=Son İstek adminUserSettings.usage=Kullanımı Görüntüle -adminUserSettings.teams=View/Edit Teams -adminUserSettings.team=Team -adminUserSettings.manageTeams=Manage Teams -adminUserSettings.createTeam=Create Team -adminUserSettings.viewTeam=View Team -adminUserSettings.deleteTeam=Delete Team -adminUserSettings.teamName=Team Name -adminUserSettings.teamExists=Team already exists -adminUserSettings.teamCreated=Team created successfully -adminUserSettings.teamChanged=User's team was updated -adminUserSettings.teamHidden=Hidden -adminUserSettings.totalMembers=Total Members +adminUserSettings.teams=Takımları Görüntüle/Düzenle +adminUserSettings.team=Takım +adminUserSettings.manageTeams=Takımları Yönet +adminUserSettings.createTeam=Takım Oluştur +adminUserSettings.viewTeam=Takımı Görüntüle +adminUserSettings.deleteTeam=Takımı Sil +adminUserSettings.teamName=Takım Adı +adminUserSettings.teamExists=Takım zaten mevcut +adminUserSettings.teamCreated=Takım başarıyla oluşturuldu +adminUserSettings.teamChanged=Kullanıcının takımı güncellendi +adminUserSettings.teamHidden=Gizli +adminUserSettings.totalMembers=Toplam Üye adminUserSettings.confirmDeleteTeam=Bu takımı silmek istediğinizden emin misiniz? teamCreated=Takım başarıyla oluşturuldu @@ -538,7 +540,7 @@ endpointStatistics.home=Ana Sayfa endpointStatistics.login=Giriş endpointStatistics.top=En Çok endpointStatistics.numberOfVisits=Ziyaret Sayısı -endpointStatistics.visitsTooltip=Ziyaret: {0} (toplamın %{1}’i) +endpointStatistics.visitsTooltip=Ziyaret: {0} (toplamın %{1}'i) endpointStatistics.retry=Yeniden Dene database.title=Veri Tabanını İçe/Dışa Aktar @@ -604,6 +606,22 @@ home.imageToPdf.title=Resimden PDF'e home.imageToPdf.desc=Bir resmi (PNG, JPEG, GIF) PDF'e dönüştürün. imageToPdf.tags=dönüşüm,img,jpg,fotoğraf,resim +home.cbzToPdf.title=CBZ to PDF +home.cbzToPdf.desc=Convert CBZ comic book archives to PDF format. +cbzToPdf.tags=conversion,comic,book,archive,cbz,zip + +home.cbrToPdf.title=CBR to PDF +home.cbrToPdf.desc=Convert CBR comic book archives to PDF format. +cbrToPdf.tags=conversion,comic,book,archive,cbr,rar + +home.pdfToCbz.title=PDF to CBZ +home.pdfToCbz.desc=Convert PDF files to CBZ comic book archives. +pdfToCbz.tags=conversion,comic,book,archive,cbz,pdf + +home.pdfToCbr.title=PDF to CBR +home.pdfToCbr.desc=Convert PDF files to CBR comic book archives. +pdfToCbr.tags=conversion,comic,book,archive,cbr,rar + home.pdfToImage.title=PDF'den Resme home.pdfToImage.desc=PDF'yi bir resme dönüştürün. (PNG, JPEG, GIF) pdfToImage.tags=dönüşüm,img,jpg,fotoğraf,resim @@ -617,9 +635,9 @@ home.addImage.title=Resim Ekle home.addImage.desc=PDF'e belirli bir konuma resim ekler addImage.tags=img,jpg,fotoğraf,resim -home.attachments.title=Add Attachments -home.attachments.desc=Add or remove embedded files (attachments) to/from a PDF -attachments.tags=embed,attach,file,attachment,attachments +home.attachments.title=Ekleri Ekle +home.attachments.desc=PDF'ye gömülü dosyalar (ekler) ekle veya kaldır +attachments.tags=gömme,ekle,dosya,ek,ekler home.watermark.title=Filigran Ekle home.watermark.desc=PDF belgenize özel bir filigran ekleyin. @@ -772,21 +790,21 @@ home.HTMLToPDF.desc=Herhangi bir HTML dosyasını veya zip'i PDF'e dönüştür HTMLToPDF.tags=biçimlendirme,web-içeriği,dönüşüm,dönüştür #eml-to-pdf -home.EMLToPDF.title=Email to PDF -home.EMLToPDF.desc=Converts email (EML) files to PDF format including headers, body, and inline images -EMLToPDF.tags=email,conversion,eml,message,transformation,convert,mail +home.EMLToPDF.title=E-postayı PDF'ye Dönüştür +home.EMLToPDF.desc=Başlıklar, gövde ve satır içi resimler dahil olmak üzere e-posta (EML) dosyalarını PDF formatına dönüştürür +EMLToPDF.tags=e-posta, dönüşüm, eml, mesaj, dönüşüm, dönüştür, posta -EMLToPDF.title=Email To PDF -EMLToPDF.header=Email To PDF -EMLToPDF.submit=Convert -EMLToPDF.downloadHtml=Download HTML intermediate file instead of PDF -EMLToPDF.downloadHtmlHelp=This allows you to see the HTML version before PDF conversion and can help debug formatting issues -EMLToPDF.includeAttachments=Include attachments in PDF -EMLToPDF.maxAttachmentSize=Maximum attachment size (MB) -EMLToPDF.help=Converts email (EML) files to PDF format including headers, body, and inline images -EMLToPDF.troubleshootingTip1=Email to HTML is a more reliable process, so with batch-processing it is recommended to save both -EMLToPDF.troubleshootingTip2=With a small number of Emails, if the PDF is malformed, you can download HTML and override some of the problematic HTML/CSS code. -EMLToPDF.troubleshootingTip3=Embeddings, however, do not work with HTMLs +EMLToPDF.title=E-postayı PDF'ye Dönüştür +EMLToPDF.header=E-postayı PDF'ye Dönüştür +EMLToPDF.submit=Dönüştür +EMLToPDF.downloadHtml=PDF yerine HTML ara dosyasını indir +EMLToPDF.downloadHtmlHelp=Bu, PDF dönüşümünden önce HTML sürümünü görmenizi sağlar ve biçimlendirme sorunlarını çözmeye yardımcı olabilir +EMLToPDF.includeAttachments=PDF'ye ekleri dahil et +EMLToPDF.maxAttachmentSize=Maksimum ek boyutu (MB) +EMLToPDF.help=Başlıklar, gövde ve satır içi resimler dahil olmak üzere e-posta (EML) dosyalarını PDF formatına dönüştürür +EMLToPDF.troubleshootingTip1=E-postayı HTML'ye dönüştürmek daha güvenilir bir işlemdir, bu nedenle toplu işleme yaparken her ikisini de kaydetmek önerilir +EMLToPDF.troubleshootingTip2=Az sayıda e-posta için, PDF bozuksa HTML dosyasını indirip bazı sorunlu HTML/CSS kodlarını değiştirebilirsiniz +EMLToPDF.troubleshootingTip3=Ancak, gömülü içerikler HTML ile çalışmaz home.MarkdownToPDF.title=Markdown'dan PDF'e home.MarkdownToPDF.desc=Herhangi bir Markdown dosyasını PDF'e dönüştürür @@ -876,6 +894,12 @@ replace-color.selectText.8=Siyah arka plan üzerine sarı metin replace-color.selectText.9=Siyah arka plan üzerine yeşil metin replace-color.selectText.10=Metin Rengini Seç replace-color.selectText.11=Arka Plan Rengini Seç +replace-color.selectText.12=Colour Space Conversion (CMYK for Printing) +replace-color.selectText.13=CMYK Colour Space Conversion +replace-color.selectText.14=This option converts the PDF from RGB colour space to CMYK colour space, which is optimized for professional printing. This process: +replace-color.selectText.15=Converts colours to CMYK (Cyan, Magenta, Yellow, Black) colour model used by professional printers +replace-color.selectText.16=Optimizes the PDF for print production with prepress settings +replace-color.selectText.17=May result in slight colour changes as CMYK has a smaller colour gamut than RGB replace-color.submit=Değiştir @@ -907,8 +931,8 @@ login.userIsDisabled=Kullanıcı devre dışı bırakıldı, şu anda bu kullan login.alreadyLoggedIn=Zaten şu cihazlarda oturum açılmış: login.alreadyLoggedIn2=Lütfen bu cihazlardan çıkış yaparak tekrar deneyin. login.toManySessions=Çok fazla aktif oturumunuz var -login.logoutMessage=You have been logged out. -login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator. +login.logoutMessage=Oturumunuz kapatıldı. +login.invalidInResponseTo=İstenen SAML yanıtı geçersiz veya süresi dolmuş. Lütfen yöneticiyle iletişime geçin. #auto-redact autoRedact.title=Otomatik Karartma @@ -974,28 +998,28 @@ getPdfInfo.title=PDF Hakkında Bilgi Al getPdfInfo.header=PDF Hakkında Bilgi Al getPdfInfo.submit=Bilgi Al getPdfInfo.downloadJson=JSON İndir -getPdfInfo.summary=PDF Summary -getPdfInfo.summary.encrypted=This PDF is encrypted so may face issues with some applications -getPdfInfo.summary.permissions=This PDF has {0} restricted permissions which may limit what you can do with it -getPdfInfo.summary.compliance=This PDF complies with the {0} standard -getPdfInfo.summary.basicInfo=Basic Information -getPdfInfo.summary.docInfo=Document Information -getPdfInfo.summary.encrypted.alert=Encrypted PDF - This document is password protected -getPdfInfo.summary.not.encrypted.alert=Unencrypted PDF - No password protection -getPdfInfo.summary.permissions.alert=Restricted Permissions - {0} actions are not allowed -getPdfInfo.summary.all.permissions.alert=All Permissions Allowed -getPdfInfo.summary.compliance.alert={0} Compliant -getPdfInfo.summary.no.compliance.alert=No Compliance Standards -getPdfInfo.summary.security.section=Security Status -getPdfInfo.section.BasicInfo=Basic Information about the PDF document including file size, page count, and language -getPdfInfo.section.Metadata=Document metadata including title, author, creation date and other document properties -getPdfInfo.section.DocumentInfo=Technical details about the PDF document structure and version -getPdfInfo.section.Compliancy=PDF standards compliance information (PDF/A, PDF/X, etc.) -getPdfInfo.section.Encryption=Security and encryption details of the document -getPdfInfo.section.Permissions=Document permission settings that control what actions can be performed -getPdfInfo.section.Other=Additional document components like bookmarks, layers, and embedded files -getPdfInfo.section.FormFields=Interactive form fields present in the document -getPdfInfo.section.PerPageInfo=Detailed information about each page in the document +getPdfInfo.summary=PDF Özeti +getPdfInfo.summary.encrypted=Bu PDF şifreli olduğu için bazı uygulamalarda sorun yaşanabilir +getPdfInfo.summary.permissions=Bu PDF'de {0} kısıtlanmış izinler var, bu da yapabileceklerinizi sınırlayabilir +getPdfInfo.summary.compliance=Bu PDF {0} standardına uygundur +getPdfInfo.summary.basicInfo=Temel Bilgiler +getPdfInfo.summary.docInfo=Belge Bilgileri +getPdfInfo.summary.encrypted.alert=Şifreli PDF - Bu belge parola korumalıdır +getPdfInfo.summary.not.encrypted.alert=Şifresiz PDF - Parola koruması yok +getPdfInfo.summary.permissions.alert=Kısıtlanmış İzinler - {0} işlem izin verilmemiştir +getPdfInfo.summary.all.permissions.alert=Tüm İzinler Verildi +getPdfInfo.summary.compliance.alert={0} Uygun +getPdfInfo.summary.no.compliance.alert=Uygunluk Standardı Yok +getPdfInfo.summary.security.section=Güvenlik Durumu +getPdfInfo.section.BasicInfo=PDF belgesinin dosya boyutu, sayfa sayısı ve dili dahil temel bilgileri +getPdfInfo.section.Metadata=Başlık, yazar, oluşturulma tarihi ve diğer belge özelliklerini içeren belge meta verisi +getPdfInfo.section.DocumentInfo=PDF belge yapısı ve sürümü hakkında teknik detaylar +getPdfInfo.section.Compliancy=PDF standartlarına uygunluk bilgisi (PDF/A, PDF/X, vb.) +getPdfInfo.section.Encryption=Belgenin güvenlik ve şifreleme detayları +getPdfInfo.section.Permissions=Hangi işlemlerin yapılabileceğini kontrol eden belge izin ayarları +getPdfInfo.section.Other=Yer imleri, katmanlar ve gömülü dosyalar gibi ek belge bileşenleri +getPdfInfo.section.FormFields=Belgede bulunan etkileşimli form alanları +getPdfInfo.section.PerPageInfo=Belgedeki her sayfa hakkında ayrıntılı bilgiler #markdown-to-pdf @@ -1007,9 +1031,9 @@ MarkdownToPDF.credit=WeasyPrint Kullanıyor #pdf-to-markdown -PDFToMarkdown.title=PDF To Markdown -PDFToMarkdown.header=PDF To Markdown -PDFToMarkdown.submit=Convert +PDFToMarkdown.title=PDF'den Markdown'a +PDFToMarkdown.header=PDF'den Markdown'a +PDFToMarkdown.submit=Dönüştür #url-to-pdf @@ -1081,7 +1105,7 @@ addPageNumbers.selectText.5=Numaralandırılacak Sayfalar addPageNumbers.selectText.6=Özel Metin addPageNumbers.customTextDesc=Özel Metin addPageNumbers.numberPagesDesc=Hangi sayfaların numaralandırılacağını, varsayılan 'all', ayrıca 1-5 veya 2,5,9 vb. kabul eder -addPageNumbers.customNumberDesc=Varsayılan {n}, ayrıca 'Sayfa {n} / {total}', 'Metin-{n}', '{filename}-{n} kabul eder +addPageNumbers.customNumberDesc=Varsayılan {n}, ayrıca 'Sayfa {n} / {total}', 'Metin-{n}', '{filename}-{n}' kabul eder addPageNumbers.submit=Sayfa Numaraları Ekle @@ -1136,7 +1160,7 @@ pageLayout.submit=Gönder scalePages.title=Sayfa Ölçeğini Ayarla scalePages.header=Sayfa Ölçeğini Ayarla scalePages.pageSize=Belgenin bir sayfa boyutu. -scalePages.keepPageSize=Original Size +scalePages.keepPageSize=Orijinal Boyut scalePages.scaleFactor=Bir sayfanın yakınlaştırma seviyesi (kırpma). scalePages.submit=Gönder @@ -1156,7 +1180,7 @@ certSign.showSig=İmzayı Göster certSign.reason=Neden certSign.location=Konum certSign.name=İsim -certSign.showLogo=Show Logo +certSign.showLogo=Logoyu Göster certSign.submit=PDF'i İmzala @@ -1171,7 +1195,7 @@ removeCertSign.submit=İmzayı Kaldır removeBlanks.title=Boşları Kaldır removeBlanks.header=Boş Sayfaları Kaldır removeBlanks.threshold=Pixel Beyazlık Eşiği: -removeBlanks.thresholdDesc=Bir beyaz pixelin 'Beyaz' olarak sınıflandırılması için ne kadar beyaz olması gerektiğini belirlemek için eşik. 0 = Siyah, 255 saf beyaz. +removeBlanks.thresholdDesc=Bir beyaz pixelin 'Beyaz' olarak sınıflandırılması için ne kadar beyaz olması gerektiğini belirlemek için eşik. 0=Siyah, 255 saf beyaz. removeBlanks.whitePercent=Beyaz Yüzde (%): removeBlanks.whitePercentDesc=Bir sayfanın 'beyaz' pixel olması gereken yüzdesi removeBlanks.submit=Boşları Kaldır @@ -1217,6 +1241,7 @@ sign.previous=Önceki sayfa sign.maintainRatio=Oranı korumayı değiştir sign.undo=Geri Al sign.redo=Yinele +sign.colour=Signature Colour #repair repair.title=Onar @@ -1287,8 +1312,8 @@ compress.title=Sıkıştır compress.header=PDF'i Sıkıştır compress.credit=Bu hizmet PDF Sıkıştırma/Optimizasyonu için qpdf kullanır. compress.grayscale.label=Sıkıştırma için Gri Ton Uygula -compress.selectText.1=Compression Settings -compress.selectText.1.1=1-3 PDF compression,
4-6 lite image compression,
7-9 intense image compression Will dramatically reduce image quality +compress.selectText.1=Sıkıştırma Ayarları +compress.selectText.1.1=1-3 PDF sıkıştırma,
4-6 hafif görüntü sıkıştırma,
7-9 yoğun görüntü sıkıştırma Görüntü kalitesini ciddi şekilde düşürecektir compress.selectText.2=Optimizasyon seviyesi: compress.selectText.4=Otomatik mod - PDF'in tam boyutuna ulaşmak için kaliteyi otomatik ayarlar compress.selectText.5=Beklenen PDF Boyutu (örn. 25MB, 10.8MB, 25KB) @@ -1303,11 +1328,11 @@ addImage.upload=Resim ekle addImage.submit=Resim ekle #attachments -attachments.title=Add Attachments -attachments.header=Add attachments -attachments.description=Allows you to add attachments to the PDF -attachments.descriptionPlaceholder=Enter a description for the attachments... -attachments.addButton=Add Attachments +attachments.title=Ekler Ekle +attachments.header=Ekler Ekle +attachments.description=PDF'ye ekler eklemenizi sağlar +attachments.descriptionPlaceholder=Ekler için bir açıklama girin... +attachments.addButton=Ekleri Ekle #merge merge.title=Birleştir @@ -1315,7 +1340,7 @@ merge.header=Çoklu PDF'leri Birleştir (2+) merge.sortByName=İsme göre sırala merge.sortByDate=Tarihe göre sırala merge.removeCertSign=Birleştirilen dosyadaki dijital imza kaldırılsın mı? -merge.generateToc=Generate table of contents in the merged file? +merge.generateToc=Birleştirilen dosyada içindekiler tablosu oluşturulsun mu? merge.submit=Birleştir @@ -1423,6 +1448,34 @@ imageToPDF.selectText.3=Çoklu dosya mantığı (Yalnızca birden fazla resimle imageToPDF.selectText.4=Tek bir PDF'e birleştir imageToPDF.selectText.5=Ayrı PDF'lere dönüştür +#cbzToPDF +cbzToPDF.title=CBZ to PDF +cbzToPDF.header=CBZ to PDF +cbzToPDF.submit=Convert to PDF +cbzToPDF.selectText=Select CBZ file +cbzToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBZ +pdfToCBZ.title=PDF to CBZ +pdfToCBZ.header=PDF to CBZ +pdfToCBZ.submit=Convert to CBZ +pdfToCBZ.selectText=Select PDF file +pdfToCBZ.dpi=DPI (Dots Per Inch) + +#cbrToPDF +cbrToPDF.title=CBR to PDF +cbrToPDF.header=CBR to PDF +cbrToPDF.submit=Convert to PDF +cbrToPDF.selectText=Select CBR file +cbrToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBR +pdfToCBR.title=PDF to CBR +pdfToCBR.header=PDF to CBR +pdfToCBR.submit=Convert to CBR +pdfToCBR.selectText=Select PDF file +pdfToCBR.dpi=DPI (Dots Per Inch) +pdfToCBR.dpiHelp=Higher DPI results in better quality but larger file size. #pdfToImage pdfToImage.title=PDF'den Resme @@ -1435,10 +1488,11 @@ pdfToImage.colorType=Renk türü pdfToImage.color=Renk pdfToImage.grey=Gri tonlama pdfToImage.blackwhite=Siyah ve Beyaz (Veri kaybolabilir!) -pdfToImage.dpi=DPI (The server limit is {0} dpi) +pdfToImage.dpi=DPI (Sunucu limiti {0} dpi) pdfToImage.submit=Dönüştür pdfToImage.info=Python kurulu değil. WebP dönüşümü için gereklidir. pdfToImage.placeholder=(örneğin 1,2,8 veya 4,7,12-16 ya da 2n-1) +pdfToImage.includeAnnotations=Include annotations (comments, highlights etc.) #addPassword @@ -1652,9 +1706,9 @@ survey.meeting.1=Eğer Stirling PDF'i iş yerinizde kullanıyorsanız, sizinle g survey.meeting.2=Bu fırsat sayesinde: survey.meeting.3=Kurulum, entegrasyonlar veya sorun giderme konularında yardım alabilirsiniz survey.meeting.4=Performans, uç durumlar ve eksik özellikler hakkında doğrudan geri bildirim sağlayabilirsiniz -survey.meeting.5=Stirling PDF’i gerçek dünya kurumsal kullanımı için daha iyi hale getirmemize yardımcı olabilirsiniz +survey.meeting.5=Stirling PDF'i gerçek dünya kurumsal kullanımı için daha iyi hale getirmemize yardımcı olabilirsiniz survey.meeting.6=İlgileniyorsanız, ekibimizden doğrudan zaman ayırabilirsiniz. (Yalnızca İngilizce) -survey.meeting.7=Kullanım senaryolarınızı dinlemeyi ve Stirling PDF’i daha da iyi hale getirmeyi sabırsızlıkla bekliyoruz! +survey.meeting.7=Kullanım senaryolarınızı dinlemeyi ve Stirling PDF'i daha da iyi hale getirmeyi sabırsızlıkla bekliyoruz! survey.meeting.notInterested=Kurumsal kullanıcı değilseniz ve/veya görüşmeye ilgi duymuyorsanız survey.meeting.button=Görüşme Planla @@ -1740,23 +1794,23 @@ validateSignature.cert.keySize=Anahtar Boyutu validateSignature.cert.version=Sürüm validateSignature.cert.keyUsage=Anahtar Kullanımı validateSignature.cert.selfSigned=Kendi Kendine İmzalı -validateSignature.cert.bits=bits +validateSignature.cert.bits=bit # Audit Dashboard -audit.dashboard.title=Audit Dashboard -audit.dashboard.systemStatus=Audit System Status +audit.dashboard.title=Denetim Kontrol Paneli +audit.dashboard.systemStatus=Denetim Sistemi Durumu audit.dashboard.status=Durum audit.dashboard.enabled=Etkin audit.dashboard.disabled=Devre Dışı -audit.dashboard.currentLevel=Current Level -audit.dashboard.retentionPeriod=Retention Period -audit.dashboard.days=days -audit.dashboard.totalEvents=Total Events +audit.dashboard.currentLevel=Mevcut Seviye +audit.dashboard.retentionPeriod=Saklama Süresi +audit.dashboard.days=gün +audit.dashboard.totalEvents=Toplam Olay # Audit Dashboard Tabs -audit.dashboard.tab.dashboard=Dashboard -audit.dashboard.tab.events=Audit Events -audit.dashboard.tab.export=Export +audit.dashboard.tab.dashboard=Gösterge Paneli +audit.dashboard.tab.events=Denetim Olayları +audit.dashboard.tab.export=Dışa Aktar # Dashboard Charts audit.dashboard.eventsByType=Türüne Göre Olaylar audit.dashboard.eventsByUser=Kullanıcıya Göre Olaylar @@ -1766,23 +1820,23 @@ audit.dashboard.period.30days=30 Gün audit.dashboard.period.90days=90 Gün # Events Tab -audit.dashboard.auditEvents=Audit Events -audit.dashboard.filter.eventType=Event Type -audit.dashboard.filter.allEventTypes=All event types -audit.dashboard.filter.user=User -audit.dashboard.filter.userPlaceholder=Filter by user -audit.dashboard.filter.startDate=Start Date -audit.dashboard.filter.endDate=End Date -audit.dashboard.filter.apply=Apply Filters -audit.dashboard.filter.reset=Reset Filters +audit.dashboard.auditEvents=Denetim Olayları +audit.dashboard.filter.eventType=Olay Türü +audit.dashboard.filter.allEventTypes=Tüm olay türleri +audit.dashboard.filter.user=Kullanıcı +audit.dashboard.filter.userPlaceholder=Kullanıcıya göre filtrele +audit.dashboard.filter.startDate=Başlangıç Tarihi +audit.dashboard.filter.endDate=Bitiş Tarihi +audit.dashboard.filter.apply=Filtreleri Uygula +audit.dashboard.filter.reset=Filtreleri Sıfırla # Table Headers audit.dashboard.table.id=ID -audit.dashboard.table.time=Time -audit.dashboard.table.user=User -audit.dashboard.table.type=Type -audit.dashboard.table.details=Details -audit.dashboard.table.viewDetails=View Details +audit.dashboard.table.time=Zaman +audit.dashboard.table.user=Kullanıcı +audit.dashboard.table.type=Tür +audit.dashboard.table.details=Detaylar +audit.dashboard.table.viewDetails=Detayları Görüntüle # Pagination audit.dashboard.pagination.show=Göster @@ -1792,10 +1846,10 @@ audit.dashboard.pagination.pageInfo2=/ audit.dashboard.pagination.totalRecords=Toplam kayıt: # Modal -audit.dashboard.modal.eventDetails=Event Details +audit.dashboard.modal.eventDetails=Olay Detayları audit.dashboard.modal.id=ID audit.dashboard.modal.user=Kullanıcı -audit.dashboard.modal.type=Type +audit.dashboard.modal.type=Tip audit.dashboard.modal.time=Zaman audit.dashboard.modal.data=Veri @@ -1824,8 +1878,8 @@ audit.dashboard.js.loadingPage=Sayfa yükleniyor # Cookie banner # #################### cookieBanner.popUp.title=Çerezleri Nasıl Kullanıyoruz -cookieBanner.popUp.description.1=Stirling PDF’yi sizin için daha iyi çalıştırmak için çerezler ve diğer teknolojileri kullanıyoruz — araçlarımızı geliştirmemize ve seveceğiniz özellikler oluşturmamıza yardımcı oluyorlar. -cookieBanner.popUp.description.2=İstemiyorsanız, ‘Hayır Teşekkürler’ butonuna tıklayarak yalnızca temel, gerekli çerezleri etkinleştirebilirsiniz. +cookieBanner.popUp.description.1=Stirling PDF'yi sizin için daha iyi çalıştırmak için çerezler ve diğer teknolojileri kullanıyoruz — araçlarımızı geliştirmemize ve seveceğiniz özellikler oluşturmamıza yardımcı oluyorlar. +cookieBanner.popUp.description.2=İstemiyorsanız, 'Hayır Teşekkürler' butonuna tıklayarak yalnızca temel, gerekli çerezleri etkinleştirebilirsiniz. cookieBanner.popUp.acceptAllBtn=Tamam cookieBanner.popUp.acceptNecessaryBtn=Hayır Teşekkürler cookieBanner.popUp.showPreferencesBtn=Tercihleri Yönet @@ -1846,20 +1900,20 @@ cookieBanner.preferencesModal.analytics.title=Analitik cookieBanner.preferencesModal.analytics.description=Bu çerezler, araçlarımızın nasıl kullanıldığını anlamamıza yardımcı olur, böylece topluluğumuzun en çok değer verdiği özellikleri geliştirmeye odaklanabiliriz. İçiniz rahat olsun — Stirling PDF, belgelerinizin içeriğini asla takip etmez ve etmeyecektir. #scannerEffect -scannerEffect.title=Scanner Effect -scannerEffect.header=Scanner Effect -scannerEffect.description=Create a PDF that looks like it was scanned -scannerEffect.selectPDF=Select PDF: -scannerEffect.quality=Scan Quality -scannerEffect.quality.low=Low -scannerEffect.quality.medium=Medium -scannerEffect.quality.high=High -scannerEffect.rotation=Rotation Angle -scannerEffect.rotation.none=None -scannerEffect.rotation.slight=Slight -scannerEffect.rotation.moderate=Moderate -scannerEffect.rotation.severe=Severe -scannerEffect.submit=Create Scanner Effect +scannerEffect.title=Tarayıcı Efekti +scannerEffect.header=Tarayıcı Efekti +scannerEffect.description=Taranmış gibi görünen bir PDF oluştur +scannerEffect.selectPDF=PDF Seç: +scannerEffect.quality=Tarama Kalitesi +scannerEffect.quality.low=Düşük +scannerEffect.quality.medium=Orta +scannerEffect.quality.high=Yüksek +scannerEffect.rotation=Döndürme Açısı +scannerEffect.rotation.none=Yok +scannerEffect.rotation.slight=Hafif +scannerEffect.rotation.moderate=Orta +scannerEffect.rotation.severe=Şiddetli +scannerEffect.submit=Tarayıcı Efekti Oluştur #home.scannerEffect home.scannerEffect.title=Sahte Tarama @@ -1893,12 +1947,12 @@ editTableOfContents.replaceExisting=Mevcut yer işaretlerini değiştir (var ola editTableOfContents.editorTitle=Yer İşareti Düzenleyici editTableOfContents.editorDesc=Aşağıdan yer işaretleri ekleyin ve düzenleyin. Alt yer işareti eklemek için + simgesine tıklayın. editTableOfContents.addBookmark=Yeni Yer İşareti Ekle -editTableOfContents.importBookmarksDefault=Import -editTableOfContents.importBookmarksFromJsonFile=Upload JSON file -editTableOfContents.importBookmarksFromClipboard=Paste from clipboard -editTableOfContents.exportBookmarksDefault=Export -editTableOfContents.exportBookmarksAsJson=Download as JSON -editTableOfContents.exportBookmarksAsText=Copy as text +editTableOfContents.importBookmarksDefault=İçe Aktar +editTableOfContents.importBookmarksFromJsonFile=JSON dosyası yükle +editTableOfContents.importBookmarksFromClipboard=Panodan yapıştır +editTableOfContents.exportBookmarksDefault=Dışa Aktar +editTableOfContents.exportBookmarksAsJson=JSON olarak indir +editTableOfContents.exportBookmarksAsText=Metin olarak kopyala editTableOfContents.desc.1=Bu araç, bir PDF belgesine içindekiler tablosu (yer işaretleri) eklemenizi veya mevcut olanları düzenlemenizi sağlar. editTableOfContents.desc.2=Alt yer işaretleri ekleyerek hiyerarşik bir yapı oluşturabilirsiniz. editTableOfContents.desc.3=Her yer işareti bir başlık ve hedef sayfa numarası gerektirir. diff --git a/app/core/src/main/resources/messages_uk_UA.properties b/app/core/src/main/resources/messages_uk_UA.properties index cf0cc7115..a675e9ed8 100644 --- a/app/core/src/main/resources/messages_uk_UA.properties +++ b/app/core/src/main/resources/messages_uk_UA.properties @@ -137,6 +137,7 @@ lang.yor=Yoruba addPageNumbers.fontSize=Розмір шрифту addPageNumbers.fontName=Назва шрифту +addPageNumbers.fontColor=Font Colour pdfPrompt=Оберіть PDF(и) multiPdfPrompt=Оберіть PDFи (2+) multiPdfDropPrompt=Оберіть (або перетягніть) всі необхідні PDFи @@ -193,6 +194,7 @@ error.fileFormatRequired=File must be in {0} format error.invalidFormat=Invalid {0} format: {1} error.endpointDisabled=This endpoint has been disabled by the admin error.urlNotReachable=URL is not reachable, please provide a valid URL +error.invalidUrlFormat=Invalid URL format provided. The provided format is invalid. # DPI and image rendering messages - used by frontend for dynamic translation # Backend sends: [TRANSLATE:messageKey:arg1,arg2] English message @@ -604,6 +606,22 @@ home.imageToPdf.title=Зображення в PDF home.imageToPdf.desc=Перетворення зображення (PNG, JPEG, GIF) в PDF. imageToPdf.tags=конвертація,зображення,jpg,картинка,фото +home.cbzToPdf.title=CBZ to PDF +home.cbzToPdf.desc=Convert CBZ comic book archives to PDF format. +cbzToPdf.tags=conversion,comic,book,archive,cbz,zip + +home.cbrToPdf.title=CBR to PDF +home.cbrToPdf.desc=Convert CBR comic book archives to PDF format. +cbrToPdf.tags=conversion,comic,book,archive,cbr,rar + +home.pdfToCbz.title=PDF to CBZ +home.pdfToCbz.desc=Convert PDF files to CBZ comic book archives. +pdfToCbz.tags=conversion,comic,book,archive,cbz,pdf + +home.pdfToCbr.title=PDF to CBR +home.pdfToCbr.desc=Convert PDF files to CBR comic book archives. +pdfToCbr.tags=conversion,comic,book,archive,cbr,rar + home.pdfToImage.title=PDF в зображення home.pdfToImage.desc=Перетворення PDF в зображення. (PNG, JPEG, GIF) pdfToImage.tags=конвертація,зображення,jpg,картинка,фото @@ -876,6 +894,12 @@ replace-color.selectText.8=жовтий текст на чорному тлі replace-color.selectText.9=зелений текст на чорному тлі replace-color.selectText.10=Вибрати колір тексту replace-color.selectText.11=Вибрати колір тла +replace-color.selectText.12=Colour Space Conversion (CMYK for Printing) +replace-color.selectText.13=CMYK Colour Space Conversion +replace-color.selectText.14=This option converts the PDF from RGB colour space to CMYK colour space, which is optimized for professional printing. This process: +replace-color.selectText.15=Converts colours to CMYK (Cyan, Magenta, Yellow, Black) colour model used by professional printers +replace-color.selectText.16=Optimizes the PDF for print production with prepress settings +replace-color.selectText.17=May result in slight colour changes as CMYK has a smaller colour gamut than RGB replace-color.submit=Замінити @@ -1217,6 +1241,7 @@ sign.previous=Попередня сторінка sign.maintainRatio=Переключити збереження пропорцій sign.undo=Скасувати sign.redo=Повторити +sign.colour=Signature Colour #repair repair.title=Ремонт @@ -1423,6 +1448,34 @@ imageToPDF.selectText.3=Логіка для кількох файлів (акт imageToPDF.selectText.4=Об'єднати в один PDF imageToPDF.selectText.5=Перетворення в окремі PDF-файли +#cbzToPDF +cbzToPDF.title=CBZ to PDF +cbzToPDF.header=CBZ to PDF +cbzToPDF.submit=Convert to PDF +cbzToPDF.selectText=Select CBZ file +cbzToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBZ +pdfToCBZ.title=PDF to CBZ +pdfToCBZ.header=PDF to CBZ +pdfToCBZ.submit=Convert to CBZ +pdfToCBZ.selectText=Select PDF file +pdfToCBZ.dpi=DPI (Dots Per Inch) + +#cbrToPDF +cbrToPDF.title=CBR to PDF +cbrToPDF.header=CBR to PDF +cbrToPDF.submit=Convert to PDF +cbrToPDF.selectText=Select CBR file +cbrToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBR +pdfToCBR.title=PDF to CBR +pdfToCBR.header=PDF to CBR +pdfToCBR.submit=Convert to CBR +pdfToCBR.selectText=Select PDF file +pdfToCBR.dpi=DPI (Dots Per Inch) +pdfToCBR.dpiHelp=Higher DPI results in better quality but larger file size. #pdfToImage pdfToImage.title=PDF в зображення @@ -1439,6 +1492,7 @@ pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Конвертувати pdfToImage.info=Python не встановлено. Необхідно для конвертації WebP. pdfToImage.placeholder=(наприклад 1,2,8 або 4,7,12-16 або 2n-1) +pdfToImage.includeAnnotations=Include annotations (comments, highlights etc.) #addPassword diff --git a/app/core/src/main/resources/messages_vi_VN.properties b/app/core/src/main/resources/messages_vi_VN.properties index ba7ba416b..41245f2de 100644 --- a/app/core/src/main/resources/messages_vi_VN.properties +++ b/app/core/src/main/resources/messages_vi_VN.properties @@ -137,6 +137,7 @@ lang.yor=Yoruba addPageNumbers.fontSize=Font Size addPageNumbers.fontName=Font Name +addPageNumbers.fontColor=Font Colour pdfPrompt=Chọn (các) tệp PDF multiPdfPrompt=Chọn các tệp PDF (2+) multiPdfDropPrompt=Chọn (hoặc kéo và thả) tất cả các tệp PDF bạn cần @@ -193,6 +194,7 @@ error.fileFormatRequired=File must be in {0} format error.invalidFormat=Invalid {0} format: {1} error.endpointDisabled=This endpoint has been disabled by the admin error.urlNotReachable=URL is not reachable, please provide a valid URL +error.invalidUrlFormat=Invalid URL format provided. The provided format is invalid. # DPI and image rendering messages - used by frontend for dynamic translation # Backend sends: [TRANSLATE:messageKey:arg1,arg2] English message @@ -604,6 +606,22 @@ home.imageToPdf.title=Hình ảnh sang PDF home.imageToPdf.desc=Chuyển đổi hình ảnh (PNG, JPEG, GIF) sang PDF. imageToPdf.tags=chuyển đổi,img,jpg,hình ảnh,ảnh +home.cbzToPdf.title=CBZ to PDF +home.cbzToPdf.desc=Convert CBZ comic book archives to PDF format. +cbzToPdf.tags=conversion,comic,book,archive,cbz,zip + +home.cbrToPdf.title=CBR to PDF +home.cbrToPdf.desc=Convert CBR comic book archives to PDF format. +cbrToPdf.tags=conversion,comic,book,archive,cbr,rar + +home.pdfToCbz.title=PDF to CBZ +home.pdfToCbz.desc=Convert PDF files to CBZ comic book archives. +pdfToCbz.tags=conversion,comic,book,archive,cbz,pdf + +home.pdfToCbr.title=PDF to CBR +home.pdfToCbr.desc=Convert PDF files to CBR comic book archives. +pdfToCbr.tags=conversion,comic,book,archive,cbr,rar + home.pdfToImage.title=PDF sang Hình ảnh home.pdfToImage.desc=Chuyển đổi PDF sang hình ảnh. (PNG, JPEG, GIF) pdfToImage.tags=chuyển đổi,img,jpg,hình ảnh,ảnh @@ -876,6 +894,12 @@ replace-color.selectText.8=Yellow text on black background replace-color.selectText.9=Green text on black background replace-color.selectText.10=Choose text Color replace-color.selectText.11=Choose background Color +replace-color.selectText.12=Colour Space Conversion (CMYK for Printing) +replace-color.selectText.13=CMYK Colour Space Conversion +replace-color.selectText.14=This option converts the PDF from RGB colour space to CMYK colour space, which is optimized for professional printing. This process: +replace-color.selectText.15=Converts colours to CMYK (Cyan, Magenta, Yellow, Black) colour model used by professional printers +replace-color.selectText.16=Optimizes the PDF for print production with prepress settings +replace-color.selectText.17=May result in slight colour changes as CMYK has a smaller colour gamut than RGB replace-color.submit=Replace @@ -1081,7 +1105,7 @@ addPageNumbers.selectText.5=Trang cần đánh số addPageNumbers.selectText.6=Văn bản tùy chỉnh addPageNumbers.customTextDesc=Văn bản tùy chỉnh addPageNumbers.numberPagesDesc=Những trang cần đánh số, mặc định là 'all', cũng chấp nhận 1-5 hoặc 2,5,9 v.v. -addPageNumbers.customNumberDesc=Mặc định là {n}, cũng chấp nhận 'Trang {n} / {total}', 'Văn bản-{n}', '{filename}-{n} +addPageNumbers.customNumberDesc=Mặc định là {n}, cũng chấp nhận 'Trang {n} / {total}', 'Văn bản-{n}', '{filename}-{n}' addPageNumbers.submit=Thêm số trang @@ -1217,6 +1241,7 @@ sign.previous=Previous page sign.maintainRatio=Toggle maintain aspect ratio sign.undo=Undo sign.redo=Redo +sign.colour=Signature Colour #repair repair.title=Sửa chữa @@ -1423,6 +1448,34 @@ imageToPDF.selectText.3=Logic đa tệp (Chỉ được bật khi làm việc v imageToPDF.selectText.4=Trộn thành một PDF duy nhất imageToPDF.selectText.5=Chuyển đổi thành các PDF riêng biệt +#cbzToPDF +cbzToPDF.title=CBZ to PDF +cbzToPDF.header=CBZ to PDF +cbzToPDF.submit=Convert to PDF +cbzToPDF.selectText=Select CBZ file +cbzToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBZ +pdfToCBZ.title=PDF to CBZ +pdfToCBZ.header=PDF to CBZ +pdfToCBZ.submit=Convert to CBZ +pdfToCBZ.selectText=Select PDF file +pdfToCBZ.dpi=DPI (Dots Per Inch) + +#cbrToPDF +cbrToPDF.title=CBR to PDF +cbrToPDF.header=CBR to PDF +cbrToPDF.submit=Convert to PDF +cbrToPDF.selectText=Select CBR file +cbrToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBR +pdfToCBR.title=PDF to CBR +pdfToCBR.header=PDF to CBR +pdfToCBR.submit=Convert to CBR +pdfToCBR.selectText=Select PDF file +pdfToCBR.dpi=DPI (Dots Per Inch) +pdfToCBR.dpiHelp=Higher DPI results in better quality but larger file size. #pdfToImage pdfToImage.title=PDF sang hình ảnh @@ -1439,6 +1492,7 @@ pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Chuyển đổi pdfToImage.info=Python is not installed. Required for WebP conversion. pdfToImage.placeholder=(ví dụ: 1,2,8 hoặc 4,7,12-16 hoặc 2n-1) +pdfToImage.includeAnnotations=Include annotations (comments, highlights etc.) #addPassword diff --git a/app/core/src/main/resources/messages_zh_CN.properties b/app/core/src/main/resources/messages_zh_CN.properties index 80abcef7a..34222b070 100644 --- a/app/core/src/main/resources/messages_zh_CN.properties +++ b/app/core/src/main/resources/messages_zh_CN.properties @@ -137,6 +137,7 @@ lang.yor=约鲁巴语 addPageNumbers.fontSize=字体大小 addPageNumbers.fontName=字体名称 +addPageNumbers.fontColor=Font Colour pdfPrompt=选择 PDF multiPdfPrompt=选择多个 PDF(2个或更多) multiPdfDropPrompt=选择(或拖拽)所需的 PDF @@ -193,6 +194,7 @@ error.fileFormatRequired=File must be in {0} format error.invalidFormat=Invalid {0} format: {1} error.endpointDisabled=该端点被管理员禁用 error.urlNotReachable=URL无法访问,请提供有效的URL +error.invalidUrlFormat=Invalid URL format provided. The provided format is invalid. # DPI and image rendering messages - used by frontend for dynamic translation # Backend sends: [TRANSLATE:messageKey:arg1,arg2] English message @@ -604,6 +606,22 @@ home.imageToPdf.title=转换图像到 PDF home.imageToPdf.desc=将图像(PNG、JPEG、GIF)转换为 PDF。 imageToPdf.tags=转换、图像、JPG、图片、照片 +home.cbzToPdf.title=CBZ to PDF +home.cbzToPdf.desc=Convert CBZ comic book archives to PDF format. +cbzToPdf.tags=conversion,comic,book,archive,cbz,zip + +home.cbrToPdf.title=CBR to PDF +home.cbrToPdf.desc=Convert CBR comic book archives to PDF format. +cbrToPdf.tags=conversion,comic,book,archive,cbr,rar + +home.pdfToCbz.title=PDF to CBZ +home.pdfToCbz.desc=Convert PDF files to CBZ comic book archives. +pdfToCbz.tags=conversion,comic,book,archive,cbz,pdf + +home.pdfToCbr.title=PDF to CBR +home.pdfToCbr.desc=Convert PDF files to CBR comic book archives. +pdfToCbr.tags=conversion,comic,book,archive,cbr,rar + home.pdfToImage.title=转换 PDF 到图像 home.pdfToImage.desc=将 PDF 转换为图像(PNG、JPEG、GIF)。 pdfToImage.tags=转换、图像、JPG、图片、照片 @@ -876,6 +894,12 @@ replace-color.selectText.8=黑底黄字 replace-color.selectText.9=黑底绿字 replace-color.selectText.10=选择文本颜色 replace-color.selectText.11=选择背景颜色 +replace-color.selectText.12=Colour Space Conversion (CMYK for Printing) +replace-color.selectText.13=CMYK Colour Space Conversion +replace-color.selectText.14=This option converts the PDF from RGB colour space to CMYK colour space, which is optimized for professional printing. This process: +replace-color.selectText.15=Converts colours to CMYK (Cyan, Magenta, Yellow, Black) colour model used by professional printers +replace-color.selectText.16=Optimizes the PDF for print production with prepress settings +replace-color.selectText.17=May result in slight colour changes as CMYK has a smaller colour gamut than RGB replace-color.submit=取代 @@ -1217,6 +1241,7 @@ sign.previous=上一页 sign.maintainRatio=切换保持长宽比 sign.undo=撤销 sign.redo=重做 +sign.colour=Signature Colour #repair repair.title=修复 @@ -1423,6 +1448,34 @@ imageToPDF.selectText.3=多文件逻辑(仅在处理多个图像时启用) imageToPDF.selectText.4=合并成一个 PDF 文件 imageToPDF.selectText.5=转换为独立的 PDF 文件 +#cbzToPDF +cbzToPDF.title=CBZ to PDF +cbzToPDF.header=CBZ to PDF +cbzToPDF.submit=Convert to PDF +cbzToPDF.selectText=Select CBZ file +cbzToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBZ +pdfToCBZ.title=PDF to CBZ +pdfToCBZ.header=PDF to CBZ +pdfToCBZ.submit=Convert to CBZ +pdfToCBZ.selectText=Select PDF file +pdfToCBZ.dpi=DPI (Dots Per Inch) + +#cbrToPDF +cbrToPDF.title=CBR to PDF +cbrToPDF.header=CBR to PDF +cbrToPDF.submit=Convert to PDF +cbrToPDF.selectText=Select CBR file +cbrToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBR +pdfToCBR.title=PDF to CBR +pdfToCBR.header=PDF to CBR +pdfToCBR.submit=Convert to CBR +pdfToCBR.selectText=Select PDF file +pdfToCBR.dpi=DPI (Dots Per Inch) +pdfToCBR.dpiHelp=Higher DPI results in better quality but larger file size. #pdfToImage pdfToImage.title=PDF 转图片 @@ -1439,6 +1492,7 @@ pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=转换 pdfToImage.info=WebP 转换需要安装 Python pdfToImage.placeholder=(例如:1,2,8 或 4,7,12-16 或 2n-1) +pdfToImage.includeAnnotations=Include annotations (comments, highlights etc.) #addPassword diff --git a/app/core/src/main/resources/messages_zh_TW.properties b/app/core/src/main/resources/messages_zh_TW.properties index c2cf4518c..2da25a017 100644 --- a/app/core/src/main/resources/messages_zh_TW.properties +++ b/app/core/src/main/resources/messages_zh_TW.properties @@ -137,14 +137,15 @@ lang.yor=約魯巴語 addPageNumbers.fontSize=字型大小 addPageNumbers.fontName=字型名稱 +addPageNumbers.fontColor=字型顏色 pdfPrompt=選擇 PDF 檔案 multiPdfPrompt=選擇多個 PDF 檔案 multiPdfDropPrompt=選擇(或拖放)所有需要的 PDF 檔案 imgPrompt=選擇圖片 genericSubmit=送出 uploadLimit=檔案大小上限: -uploadLimitExceededSingular=太大。允許的最大檔案大小為 -uploadLimitExceededPlural=太大。允許的最大檔案大小為 +uploadLimitExceededSingular=檔案太大。允許的最大檔案大小為 +uploadLimitExceededPlural=檔案太大。允許的最大檔案大小為 processTimeWarning=警告:此過程可能長達一分鐘,具體取決於檔案大小 pageOrderPrompt=自訂頁面順序(輸入以逗號分隔的頁碼或函式,如 2n+1): pageSelectionPrompt=自訂頁面選擇(輸入以逗號分隔的頁碼 1、5、6 或 2n+1 等函式的清單): @@ -193,6 +194,7 @@ error.fileFormatRequired=檔案必須為 {0} 格式 error.invalidFormat=無效的 {0} 格式:{1} error.endpointDisabled=此端點已被管理員停用 error.urlNotReachable=無法連線至 URL,請提供有效的 URL +error.invalidUrlFormat=提供了無效的 URL 格式。 # DPI and image rendering messages - used by frontend for dynamic translation # Backend sends: [TRANSLATE:messageKey:arg1,arg2] English message @@ -325,7 +327,7 @@ pipelineOptions.validateButton=驗證 ######################## enterpriseEdition.button=升級至專業版 enterpriseEdition.warning=此功能僅提供給專業版使用者使用。 -enterpriseEdition.yamlAdvert=Stirling PDF 專業版支援 YAML 設定檔和其他單一登入 (SSO) 功能。 +enterpriseEdition.yamlAdvert=Stirling PDF 專業版支援 YAML 設定檔和其他 SSO 登入功能。 enterpriseEdition.ssoAdvert=需要更多使用者管理功能嗎?請參考 Stirling PDF 專業版 enterpriseEdition.proTeamFeatureDisabled=團隊管理功能需要專業版或更進階的授權 @@ -364,42 +366,42 @@ navbar.sections.popular=熱門功能 # SETTINGS # ############# settings.title=設定 -settings.update=有更新可用 -settings.updateAvailable=目前安裝的版本是 {0}。有新版本({1})可供使用。 +settings.update=有可用更新 +settings.updateAvailable=目前安裝的版本為 {0},已有新版本({1})可供更新。 # Update modal and notification strings -update.urgentUpdateAvailable=🚨 Update Available -update.updateAvailable=Update Available -update.modalTitle=Update Available -update.current=Current -update.latest=Latest -update.latestStable=Latest Stable -update.priority=Priority -update.recommendedAction=Recommended Action -update.breakingChangesDetected=⚠️ Breaking Changes Detected -update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. -update.migrationGuides=Migration Guides: -update.viewGuide=View Guide -update.loadingDetailedInfo=Loading detailed version information... -update.close=Close -update.viewAllReleases=View All Releases -update.downloadLatest=Download Latest -update.availableUpdates=Available Updates: -update.unableToLoadDetails=Unable to load detailed version information. -update.version=Version +update.urgentUpdateAvailable=🚨 有緊急更新 +update.updateAvailable=有可用更新 +update.modalTitle=有可用更新 +update.current=目前版本 +update.latest=最新版本 +update.latestStable=最新穩定版本 +update.priority=優先等級 +update.recommendedAction=建議操作 +update.breakingChangesDetected=⚠️ 偵測到重大變更 +update.breakingChangesMessage=此更新包含可能無法向下相容的重大變更,請先參閱下方的遷移指南。 +update.migrationGuides=遷移指南: +update.viewGuide=檢視指南 +update.loadingDetailedInfo=正在載入版本詳細資訊... +update.close=關閉 +update.viewAllReleases=檢視所有版本 +update.downloadLatest=下載最新版 +update.availableUpdates=可用更新: +update.unableToLoadDetails=無法載入版本詳細資訊。 +update.version=版本 # Update priority levels -update.priority.urgent=URGENT -update.priority.normal=NORMAL -update.priority.minor=MINOR -update.priority.low=LOW +update.priority.urgent=緊急更新 +update.priority.normal=一般更新 +update.priority.minor=次要更新 +update.priority.low=低優先度更新 # Breaking changes text -update.breakingChanges=Breaking Changes: -update.breakingChangesDefault=This version contains breaking changes -update.migrationGuide=Migration Guide +update.breakingChanges=重大變更: +update.breakingChangesDefault=此版本包含可能無法向下相容的重大變更 +update.migrationGuide=遷移指南 settings.appVersion=應用程式版本: -settings.downloadOption.title=選擇下載選項(適用於單一檔案非壓縮下載): +settings.downloadOption.title=選擇下載選項(適用於單檔無壓縮下載): settings.downloadOption.1=在同一視窗中開啟 settings.downloadOption.2=在新視窗中開啟 settings.downloadOption.3=下載檔案 @@ -578,9 +580,9 @@ home.setFavorites=設定我的最愛 home.hideFavorites=隱藏我的最愛 home.showFavorites=顯示我的最愛 home.legacyHomepage=舊版首頁 -home.newHomePage=嘗試使用全新首頁! +home.newHomePage=試試全新的首頁! home.alphabetical=按照字母排序 -home.globalPopularity=熱門程度 +home.globalPopularity=熱門度 home.sortBy=排序方式: home.multiTool.title=PDF 複合工具 @@ -604,12 +606,28 @@ home.imageToPdf.title=圖片轉 PDF home.imageToPdf.desc=將圖片(PNG、JPEG、GIF)轉換為 PDF。 imageToPdf.tags=轉換,img,jpg,圖片,照片 +home.cbzToPdf.title=CBZ to PDF +home.cbzToPdf.desc=Convert CBZ comic book archives to PDF format. +cbzToPdf.tags=conversion,comic,book,archive,cbz,zip + +home.cbrToPdf.title=CBR to PDF +home.cbrToPdf.desc=Convert CBR comic book archives to PDF format. +cbrToPdf.tags=conversion,comic,book,archive,cbr,rar + +home.pdfToCbz.title=PDF to CBZ +home.pdfToCbz.desc=Convert PDF files to CBZ comic book archives. +pdfToCbz.tags=conversion,comic,book,archive,cbz,pdf + +home.pdfToCbr.title=PDF to CBR +home.pdfToCbr.desc=Convert PDF files to CBR comic book archives. +pdfToCbr.tags=conversion,comic,book,archive,cbr,rar + home.pdfToImage.title=PDF 轉圖片 -home.pdfToImage.desc=將 PDF 轉換為圖片。(PNG、JPEG、GIF) +home.pdfToImage.desc=將 PDF 轉換為圖片(PNG、JPEG、GIF)。 pdfToImage.tags=轉換,img,jpg,圖片,照片 home.pdfOrganiser.title=整理 -home.pdfOrganiser.desc=以任何順序移除/重新排列頁面 +home.pdfOrganiser.desc=移除或重新排列頁面順序 pdfOrganiser.tags=雙面,偶數,奇數,排序,移動 @@ -699,8 +717,8 @@ home.sign.title=簽章 home.sign.desc=透過繪圖、文字或影像新增簽章到 PDF sign.tags=授權,縮寫,繪製簽章,文字,影像簽章 -home.flatten.title=平坦化 -home.flatten.desc=從 PDF 中移除所有互動元素和表單 +home.flatten.title=扁平化 +home.flatten.desc=移除 PDF 中的所有互動元素和表單 flatten.tags=靜態,停用,非互動,簡化 home.repair.title=修復 @@ -712,8 +730,8 @@ home.removeBlanks.desc=偵測並從文件中移除空白頁面 removeBlanks.tags=清理,簡化,非內容,組織 home.removeAnnotations.title=移除註釋 -home.removeAnnotations.desc=從 PDF 中移除所有註釋/註解 -removeAnnotations.tags=註釋,突出,註解,標記,移除 +home.removeAnnotations.desc=移除 PDF 中的所有註釋 +removeAnnotations.tags=註釋,醒目提示,備註,標記,移除 home.compare.title=比較 home.compare.desc=比較並顯示 2 個 PDF 檔案的差異 @@ -729,7 +747,7 @@ removeCertSign.tags=驗證,PEM,P12,官方,解密 home.pageLayout.title=多頁面版面配置 home.pageLayout.desc=將 PDF 檔案的多個頁面合併到單一頁面 -pageLayout.tags=合併,複合,單一檢視,組織 +pageLayout.tags=合併,複合,單一檢視,整理 home.scalePages.title=調整頁面大小/比例 home.scalePages.desc=修改頁面及其內容的大小/比例。 @@ -740,8 +758,8 @@ home.pipeline.desc=透過定義管道指令碼在 PDF 上執行多個操作 pipeline.tags=自動化,序列,指令碼,批次處理 home.add-page-numbers.title=新增頁碼 -home.add-page-numbers.desc=在文件的設定位置新增頁碼 -add-page-numbers.tags=分頁,標籤,組織,索引 +home.add-page-numbers.desc=在文件的指定位置新增頁碼 +add-page-numbers.tags=分頁,標籤,整理,索引 home.auto-rename.title=自動重新命名 PDF 檔案 home.auto-rename.desc=根據其偵測到的標頭自動重新命名 PDF 檔案 @@ -756,8 +774,8 @@ home.crop.desc=裁剪 PDF 以減少其大小(保持文字!) crop.tags=修剪,縮小,編輯,形狀 home.autoSplitPDF.title=自動分割頁面 -home.autoSplitPDF.desc=自動分割掃描的 PDF,使用實體掃描頁面分割器 QR Code -autoSplitPDF.tags=基於 QR Code,分離,掃描區段,組織 +home.autoSplitPDF.desc=使用實體掃描頁面分割器 QR Code 自動分割掃描的 PDF +autoSplitPDF.tags=基於 QR Code,分離,掃描區段,整理 home.sanitizePdf.title=清理 home.sanitizePdf.desc=從 PDF 檔案中移除指令碼和其他元素 @@ -806,7 +824,7 @@ home.extractPage.desc=從 PDF 中提取選定的頁面 extractPage.tags=提取 -home.PdfToSinglePage.title=PDF 轉單一大頁面 +home.PdfToSinglePage.title=PDF 轉單一大型頁面 home.PdfToSinglePage.desc=將所有 PDF 頁面合併為一個大的單一頁面 PdfToSinglePage.tags=單一頁面 @@ -860,8 +878,8 @@ home.validateSignature.desc=驗證 PDF 文件中的數位簽章與憑證 validateSignature.tags=簽章,驗證,確認,pdf,憑證,數位簽章,驗證簽章,驗證憑證 #replace-invert-color -replace-color.title=取代-反轉顏色 -replace-color.header=取代-反轉 PDF 顏色 +replace-color.title=取代與反轉顏色 +replace-color.header=取代與反轉 PDF 顏色 home.replaceColorPdf.title=取代與反轉顏色 home.replaceColorPdf.desc=取代 PDF 中文字和背景的顏色,並反轉整個 PDF 的顏色以減少檔案大小 replaceColorPdf.tags=取代顏色,頁面操作,後端,伺服器端 @@ -876,6 +894,12 @@ replace-color.selectText.8=黑底黃字 replace-color.selectText.9=黑底綠字 replace-color.selectText.10=選擇文字顏色 replace-color.selectText.11=選擇背景顏色 +replace-color.selectText.12=Colour Space Conversion (CMYK for Printing) +replace-color.selectText.13=CMYK Colour Space Conversion +replace-color.selectText.14=This option converts the PDF from RGB colour space to CMYK colour space, which is optimized for professional printing. This process: +replace-color.selectText.15=Converts colours to CMYK (Cyan, Magenta, Yellow, Black) colour model used by professional printers +replace-color.selectText.16=Optimizes the PDF for print production with prepress settings +replace-color.selectText.17=May result in slight colour changes as CMYK has a smaller colour gamut than RGB replace-color.submit=取代 @@ -893,7 +917,7 @@ login.rememberme=記住我 login.invalid=使用者名稱或密碼無效。 login.locked=您的帳號已被鎖定。 login.signinTitle=請登入 -login.ssoSignIn=透過 SSO 單一登入 +login.ssoSignIn=透過 SSO 登入 login.oAuth2AutoCreateDisabled=OAuth 2.0 自動建立使用者功能已停用 login.oAuth2AdminBlockedUser=目前不允許未註冊的使用者註冊或登入。請聯絡系統管理員。 login.oauth2RequestNotFound=找不到驗證請求 @@ -908,7 +932,7 @@ login.alreadyLoggedIn=您已經登入了 login.alreadyLoggedIn2=部裝置。請先從這些裝置登出後再試一次。 login.toManySessions=您有太多使用中的工作階段 login.logoutMessage=您已登出。 -login.invalidInResponseTo=The requested SAML response is invalid or has expired. Please contact the administrator. +login.invalidInResponseTo=所要求的 SAML 回應無效或已過期,請聯絡系統管理員。 #auto-redact autoRedact.title=自動塗黑 @@ -1049,10 +1073,10 @@ AddStampRequest.stampImage=圖章圖片 AddStampRequest.alphabet=字母表 AddStampRequest.fontSize=字型/影像大小 AddStampRequest.rotation=旋轉 -AddStampRequest.opacity=透明度 +AddStampRequest.opacity=不透明度 AddStampRequest.position=位置 -AddStampRequest.overrideX=覆蓋 X 座標 -AddStampRequest.overrideY=覆蓋 Y 座標 +AddStampRequest.overrideX=覆寫 X 座標 +AddStampRequest.overrideY=覆寫 Y 座標 AddStampRequest.customMargin=自訂邊緣 AddStampRequest.customColor=自訂文字顏色 AddStampRequest.submit=送出 @@ -1109,14 +1133,14 @@ crop.submit=送出 #autoSplitPDF autoSplitPDF.title=自動分割 PDF autoSplitPDF.header=自動分割 PDF -autoSplitPDF.description=列印,插入,掃描,上傳,讓 Stirling PDF 處理其餘的工作。不需要手動工作排序。 -autoSplitPDF.selectText.1=從下面列印一些分隔紙張(黑白即可)。 -autoSplitPDF.selectText.2=透過在它們之間插入分隔紙張一次掃描所有文件。 -autoSplitPDF.selectText.3=上傳單一大的掃描 PDF 檔案,讓 Stirling PDF 處理其餘的工作。 -autoSplitPDF.selectText.4=自動偵測並移除分隔頁面,確保最終文件整潔。 -autoSplitPDF.formPrompt=送出包含 Stirling-PDF 頁面分隔器的 PDF: +autoSplitPDF.description=列印、插入、掃描、上傳,剩下的就交給 Stirling PDF 自動處理,無需手動排序。 +autoSplitPDF.selectText.1=從下方列印分隔頁(黑白列印即可)。 +autoSplitPDF.selectText.2=將分隔頁夾在文件之間,一次掃描全部文件。 +autoSplitPDF.selectText.3=上傳完整的單一掃描 PDF 檔,剩下的交給 Stirling PDF 自動處理。 +autoSplitPDF.selectText.4=系統會自動偵測並移除分隔頁,確保輸出的文件整齊乾淨。 +autoSplitPDF.formPrompt=送出包含 Stirling PDF 分隔頁的 PDF 檔案: autoSplitPDF.duplexMode=雙面模式(正反面掃描) -autoSplitPDF.dividerDownload2=下載 '自動分割器分隔器(帶說明).pdf' +autoSplitPDF.dividerDownload2=下載《自動分割用分隔頁(含使用說明).pdf》 autoSplitPDF.submit=送出 @@ -1215,8 +1239,9 @@ sign.last=最後一頁 sign.next=下一頁 sign.previous=上一頁 sign.maintainRatio=切換維持長寬比 -sign.undo=撤銷 +sign.undo=復原 sign.redo=重做 +sign.colour=Signature Colour #repair repair.title=修復 @@ -1225,17 +1250,17 @@ repair.submit=修復 #flatten -flatten.title=平坦化 -flatten.header=PDF 平坦化 -flatten.flattenOnlyForms=僅將表單平坦化 -flatten.submit=平坦化 +flatten.title=扁平化 +flatten.header=PDF 扁平化 +flatten.flattenOnlyForms=僅將表單扁平化 +flatten.submit=扁平化 #ScannerImageSplit ScannerImageSplit.selectText.1=角度閾值: ScannerImageSplit.selectText.2=設定影像旋轉所需的最小絕對角度(預設:10)。 ScannerImageSplit.selectText.3=容忍度: -ScannerImageSplit.selectText.4=確定圍繞估計的背景顏色的顏色變化範圍(預設:30)。 +ScannerImageSplit.selectText.4=決定圍繞估計的背景顏色的顏色變化範圍(預設:30)。 ScannerImageSplit.selectText.5=最小區域: ScannerImageSplit.selectText.6=設定照片的最小區域閾值(預設:10000)。 ScannerImageSplit.selectText.7=最小輪廓區域: @@ -1246,21 +1271,21 @@ ScannerImageSplit.info=尚未安裝 Python。需要安裝 Python 才能執行。 #OCR -ocr.title=OCR / 掃描清理 -ocr.header=清理掃描 / OCR(光學字元識別) -ocr.selectText.1=選擇要在 PDF 中偵測的語言(列出的是目前可以偵測的語言): -ocr.selectText.2=產生包含 OCR 文字的文字文件,並與 OCR 的 PDF 一起 -ocr.selectText.3=修正掃描的頁面傾斜角度,將它們旋轉回原位 -ocr.selectText.4=清理頁面以降低 OCR 在背景雜訊中識別文字的機率。(無輸出變化) -ocr.selectText.5=清理頁面以降低 OCR 在背景雜訊中識別文字的機率,保持乾淨的輸出。 -ocr.selectText.6=忽略具有互動文字的頁面,只對影像頁面進行 OCR -ocr.selectText.7=強制 OCR,將對每一頁進行 OCR,移除所有原始文字元素 -ocr.selectText.8=正常(如果 PDF 包含文字將出錯) +ocr.title=OCR 與掃描清理 +ocr.header=清理掃描與 OCR(光學字元辨識) +ocr.selectText.1=選擇要在 PDF 中偵測的語言(列出的是目前可偵測的語言): +ocr.selectText.2=產生包含 OCR 文字的文字檔,並與 OCR 後的 PDF 一起提供 +ocr.selectText.3=修正掃描頁面的傾斜角度,將其旋轉回正 +ocr.selectText.4=清理頁面以降低 OCR 在背景雜訊中辨識文字的機率。(無輸出變化) +ocr.selectText.5=清理頁面以降低 OCR 在背景雜訊中辨識文字的機率,並保持乾淨的輸出。 +ocr.selectText.6=忽略具有互動文字的頁面,僅對影像頁面進行 OCR +ocr.selectText.7=強制 OCR,將對每一頁進行 OCR,並移除所有原始文字元素 +ocr.selectText.8=正常(若 PDF 包含文字將會出錯) ocr.selectText.9=額外設定 ocr.selectText.10=OCR 模式 -ocr.selectText.11=移除 OCR 後的影像(移除所有影像,只有在轉換步驟中才有用) +ocr.selectText.11=移除 OCR 後的圖片(移除所有圖片,只有在轉換步驟中有用) ocr.selectText.12=渲染類型(進階) -ocr.help=請閱讀此文件,了解如何使用其他語言和/或在 Docker 中使用 +ocr.help=請閱讀此文件,以了解如何使用其他語言或在 Docker 中使用 ocr.credit=此服務使用 qpdf 和 Tesseract 進行 OCR。 ocr.submit=使用 OCR 處理 PDF @@ -1278,26 +1303,26 @@ fileToPDF.title=檔案轉 PDF fileToPDF.header=將任何檔案轉換為 PDF fileToPDF.credit=此服務使用 LibreOffice 和 Unoconv 進行檔案轉換。 fileToPDF.supportedFileTypesInfo=支援的檔案類型 -fileToPDF.supportedFileTypes=支援的檔案類型應包括以下內容,但要獲得完整的更新支援格式列表,請參閱 LibreOffice 的文件 +fileToPDF.supportedFileTypes=支援的檔案類型應包含以下內容,但要獲得完整的更新支援格式列表,請參閱 LibreOffice 的文件 fileToPDF.submit=轉換為 PDF #compress compress.title=壓縮 compress.header=壓縮 PDF -compress.credit=此服務使用 qpdf 進行 PDF 壓縮/最佳化。 +compress.credit=此服務使用 qpdf 進行 PDF 壓縮與最佳化。 compress.grayscale.label=套用灰階進行壓縮 compress.selectText.1=壓縮設定 compress.selectText.1.1=1-3 為一般 PDF 壓縮,
4-6 為輕度圖片壓縮,
7-9 為高強度圖片壓縮,將大幅降低圖片品質 compress.selectText.2=最佳化等級: compress.selectText.4=自動模式 - 自動調整品質使 PDF 達到指定的檔案大小 -compress.selectText.5=指定的 PDF 檔案大小(例如 25MB, 10.8MB, 25KB) +compress.selectText.5=指定的 PDF 檔案大小(例如:25MB、10.8MB、25KB) compress.submit=壓縮 #Add image addImage.title=新增圖片 -addImage.header=新增圖片到 PDF +addImage.header=新增圖片至 PDF addImage.everyPage=每一頁? addImage.upload=新增圖片 addImage.submit=新增圖片 @@ -1324,12 +1349,12 @@ pdfOrganiser.title=頁面整理 pdfOrganiser.header=PDF 頁面整理 pdfOrganiser.submit=重新排列頁面 pdfOrganiser.mode=模式 -pdfOrganiser.mode.1=自定義頁面順序 +pdfOrganiser.mode.1=自訂頁面順序 pdfOrganiser.mode.2=反向順序 -pdfOrganiser.mode.3=雙工排序 +pdfOrganiser.mode.3=雙面排序 pdfOrganiser.mode.4=摺頁冊排序 -pdfOrganiser.mode.5=側裝訂摺頁冊排序 -pdfOrganiser.mode.6=奇偶拆分 +pdfOrganiser.mode.5=側邊裝訂摺頁冊排序 +pdfOrganiser.mode.6=奇偶分割 pdfOrganiser.mode.7=刪除第一頁 pdfOrganiser.mode.8=刪除最後一頁 pdfOrganiser.mode.9=刪除第一頁和最後一頁 @@ -1339,14 +1364,14 @@ pdfOrganiser.placeholder=(例如 1,3,2 或 4-8,2,10-12 或 2n-1) #multiTool -multiTool.title=PDF 複合工具 -multiTool.header=PDF 複合工具 +multiTool.title=PDF 複合式工具 +multiTool.header=PDF 複合式工具 multiTool.uploadPrompts=檔名 multiTool.selectAll=全選 multiTool.deselectAll=取消全選 multiTool.selectPages=選取頁面 multiTool.selectedPages=已選取的頁面 -multiTool.page=頁面 +multiTool.page=頁 multiTool.deleteSelected=刪除已選取的項目 multiTool.downloadAll=匯出 multiTool.downloadSelected=匯出已選取的項目 @@ -1374,7 +1399,7 @@ decrypt.serverError=解密時發生伺服器錯誤:{0} decrypt.success=檔案已成功解密。 #multiTool-advert -multiTool-advert.message=此功能也可以在我們的複合工具頁面中使用。前往查看並體驗更強大的逐頁操作介面及其他進階功能! +multiTool-advert.message=此功能也可以在我們的複合式工具頁面中使用。前往查看並體驗更強大的逐頁操作介面及其他進階功能! #view pdf viewPdf.title=檢視/編輯 PDF @@ -1423,22 +1448,51 @@ imageToPDF.selectText.3=多文件邏輯(僅在處理多個影像時啟用) imageToPDF.selectText.4=合併為單一 PDF imageToPDF.selectText.5=轉換為單獨的 PDF +#cbzToPDF +cbzToPDF.title=CBZ to PDF +cbzToPDF.header=CBZ to PDF +cbzToPDF.submit=Convert to PDF +cbzToPDF.selectText=Select CBZ file +cbzToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBZ +pdfToCBZ.title=PDF to CBZ +pdfToCBZ.header=PDF to CBZ +pdfToCBZ.submit=Convert to CBZ +pdfToCBZ.selectText=Select PDF file +pdfToCBZ.dpi=DPI (Dots Per Inch) + +#cbrToPDF +cbrToPDF.title=CBR to PDF +cbrToPDF.header=CBR to PDF +cbrToPDF.submit=Convert to PDF +cbrToPDF.selectText=Select CBR file +cbrToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript) + +#pdfToCBR +pdfToCBR.title=PDF to CBR +pdfToCBR.header=PDF to CBR +pdfToCBR.submit=Convert to CBR +pdfToCBR.selectText=Select PDF file +pdfToCBR.dpi=DPI (Dots Per Inch) +pdfToCBR.dpiHelp=Higher DPI results in better quality but larger file size. #pdfToImage pdfToImage.title=PDF 轉圖片 pdfToImage.header=PDF 轉圖片 pdfToImage.selectText=影像格式 pdfToImage.singleOrMultiple=頁面到影像的結果類型 -pdfToImage.single=單一大影像結合所有頁面 +pdfToImage.single=單一大型影像結合所有頁面 pdfToImage.multi=多個影像,每頁一個影像 pdfToImage.colorType=顏色類型 pdfToImage.color=顏色 pdfToImage.grey=灰度 pdfToImage.blackwhite=黑白(可能會遺失資料!) -pdfToImage.dpi=DPI (The server limit is {0} dpi) +pdfToImage.dpi=DPI (伺服器的 dpi 限制為 {0} dpi) pdfToImage.submit=轉換 pdfToImage.info=尚未安裝 Python。需要安裝 Python 才能進行 WebP 轉換。 pdfToImage.placeholder=(例如 1,2,8 或 4,7,12-16 或 2n-1) +pdfToImage.includeAnnotations=包含註釋(評論、醒目提示等) #addPassword @@ -1447,18 +1501,18 @@ addPassword.header=新增密碼(加密) addPassword.selectText.1=選擇要加密的 PDF addPassword.selectText.2=使用者密碼 addPassword.selectText.3=加密金鑰長度 -addPassword.selectText.4=較高的值更強,但較低的值具有更好的相容性。 +addPassword.selectText.4=較高的值更安全,但較低的值具有更好的相容性。 addPassword.selectText.5=要設定的權限(建議與擁有者密碼一起使用) addPassword.selectText.6=防止文件組裝 -addPassword.selectText.7=防止內容提取 -addPassword.selectText.8=防止為了無障礙使用而提取資料 +addPassword.selectText.7=防止內容擷取 +addPassword.selectText.8=防止為了無障礙使用而擷取內容 addPassword.selectText.9=防止填寫表單 addPassword.selectText.10=防止修改 addPassword.selectText.11=防止註釋修改 addPassword.selectText.12=防止列印 addPassword.selectText.13=防止列印不同格式 addPassword.selectText.14=擁有者密碼 -addPassword.selectText.15=限制一旦開啟文件可以做什麼(並非所有軟體都支援) +addPassword.selectText.15=限制開啟文件後的操作(並非所有軟體都支援) addPassword.selectText.16=限制開啟文件本身 addPassword.submit=加密 @@ -1485,12 +1539,12 @@ watermark.type.2=圖片 #Change permissions permissions.title=變更權限 permissions.header=變更權限 -permissions.warning=警告,要使這些權限不可變更,建議透過新增密碼頁面使用密碼設定這些權限 -permissions.selectText.1=選擇要變更權限的 PDF +permissions.warning=警告:為確保權限無法被輕易變更,建議透過「新增密碼」頁面使用密碼來設定這些權限。 +permissions.selectText.1=請選擇要變更權限的 PDF permissions.selectText.2=要設定的權限 permissions.selectText.3=防止文件組裝 -permissions.selectText.4=防止內容提取 -permissions.selectText.5=防止為了無障礙使用而提取資料 +permissions.selectText.4=防止內容擷取 +permissions.selectText.5=防止為了無障礙而擷取內容 permissions.selectText.6=防止填寫表單 permissions.selectText.7=防止修改 permissions.selectText.8=防止註釋修改 @@ -1510,17 +1564,17 @@ removePassword.submit=移除 #changeMetadata changeMetadata.title=標題: changeMetadata.header=變更中繼資料 -changeMetadata.selectText.1=請編輯你希望變更的變數 +changeMetadata.selectText.1=請編輯您希望變更的變數 changeMetadata.selectText.2=刪除所有中繼資料 changeMetadata.selectText.3=顯示自訂中繼資料: changeMetadata.author=作者: -changeMetadata.creationDate=建立日期(yyyy/MM/dd HH:mm:ss): +changeMetadata.creationDate=建立日期 (yyyy/MM/dd HH:mm:ss): changeMetadata.creator=建立者: changeMetadata.keywords=關鍵字: -changeMetadata.modDate=修改日期(yyyy/MM/dd HH:mm:ss): +changeMetadata.modDate=修改日期 (yyyy/MM/dd HH:mm:ss): changeMetadata.producer=製作人: -changeMetadata.subject=主題: -changeMetadata.trapped=陷阱: +changeMetadata.subject=主旨: +changeMetadata.trapped=補漏白: changeMetadata.selectText.4=其他中繼資料: changeMetadata.selectText.5=新增自訂中繼資料項目 changeMetadata.submit=變更 @@ -1533,11 +1587,11 @@ unlockPDFForms.submit=移除 #pdfToPDFA pdfToPDFA.title=PDF 轉 PDF/A pdfToPDFA.header=PDF 轉 PDF/A -pdfToPDFA.credit=此服務使用 libreoffice 進行 PDF/A 轉換 +pdfToPDFA.credit=此服務使用 LibreOffice 進行 PDF/A 轉換 pdfToPDFA.submit=轉換 -pdfToPDFA.tip=目前不支援上傳多個 +pdfToPDFA.tip=目前不支援上傳多個檔案 pdfToPDFA.outputFormat=輸出格式 -pdfToPDFA.pdfWithDigitalSignature=該 PDF 的憑證簽章將會在下一步被移除 +pdfToPDFA.pdfWithDigitalSignature=此 PDF 的憑證簽章將在下一步被移除 #PDFToWord @@ -1614,18 +1668,18 @@ overlay-pdfs.submit=送出 #split-by-sections split-by-sections.title=依區段分割 PDF split-by-sections.header=將 PDF 分割成區段 -split-by-sections.horizontal.label=水平劃分 -split-by-sections.vertical.label=垂直劃分 -split-by-sections.horizontal.placeholder=輸入水平劃分的數量 -split-by-sections.vertical.placeholder=輸入垂直劃分的數量 +split-by-sections.horizontal.label=水平分割 +split-by-sections.vertical.label=垂直分割 +split-by-sections.horizontal.placeholder=輸入水平分割的數量 +split-by-sections.vertical.placeholder=輸入垂直分割的數量 split-by-sections.submit=分割 PDF split-by-sections.merge=是否合併為一個 PDF #printFile printFile.title=列印檔案 -printFile.header=使用印表機印出檔案 -printFile.selectText.1=選擇要印的檔案 +printFile.header=使用印表機列印檔案 +printFile.selectText.1=選擇要列印的檔案 printFile.selectText.2=輸入印表機名稱 printFile.submit=列印 @@ -1640,16 +1694,16 @@ licenses.license=授權條款 #survey survey.nav=問卷調查 -survey.title=Stirling-PDF 問卷調查 -survey.description=Stirling-PDF 沒有追蹤功能,因此我們希望聽取使用者的意見來改進 Stirling-PDF! -survey.changes=Stirling-PDF 自上次調查以來已有所改變!欲了解更多資訊,請查看我們的部落格文章: +survey.title=Stirling PDF 問卷調查 +survey.description=Stirling PDF 沒有追蹤功能,因此我們希望聽取您的意見以改善產品! +survey.changes=Stirling PDF 自上次調查以來已有所改變!欲了解更多資訊,請查看我們的部落格文章: survey.changes2=隨著這些變更,我們正在獲得付費的商業支援和資金 survey.please=請考慮參與我們的問卷調查! -survey.disabled=(問卷調查彈出視窗將在後續更新中停用,但仍可在頁尾使用) +survey.disabled=(問卷調查彈出視窗將在後續更新中停用,但仍可在頁尾存取) survey.button=參與問卷調查 -survey.dontShowAgain=不要再次顯示 -survey.meeting.1=如果您在工作中使用 Stirling PDF,我們很希望能與您交流。我們將提供技術支援諮詢,以換取 15 分鐘的使用者體驗回饋交流。 -survey.meeting.2=這是一個機會讓您: +survey.dontShowAgain=不要再顯示 +survey.meeting.1=如果您在工作中使用 Stirling PDF,我們很希望能與您交流。我們將提供技術支援諮詢,以換取 15 分鐘的使用者體驗回饋。 +survey.meeting.2=這是一個機會,讓您: survey.meeting.3=獲得關於部署、整合或疑難排解方面的協助 survey.meeting.4=針對效能、特殊案例和缺少的功能提供直接意見回饋 survey.meeting.5=協助我們改良 Stirling PDF 以符合實際企業使用需求 @@ -1685,9 +1739,9 @@ splitByChapters.bookmarkLevel=書籤層級 splitByChapters.includeMetadata=包含中繼資料 splitByChapters.allowDuplicates=允許重複 splitByChapters.desc.1=此工具會根據 PDF 檔案的章節結構將其分割成多個 PDF。 -splitByChapters.desc.2=書籤層級:選擇用於分割的書籤層級(0 表示最上層,1 表示第二層,依此類推)。 -splitByChapters.desc.3=包含中繼資料:如果勾選,原始 PDF 的中繼資料將包含在每個分割後的 PDF 中。 -splitByChapters.desc.4=允許重複:如果勾選,允許同一頁面上的多個書籤建立獨立的 PDF。 +splitByChapters.desc.2=書籤層級:請選擇用於分割的書籤層級(0 表示最上層,1 表示第二層,依此類推)。 +splitByChapters.desc.3=包含中繼資料:若勾選,原始 PDF 的中繼資料將包含在每個分割後的 PDF 中。 +splitByChapters.desc.4=允許重複:若勾選,允許同一頁面上的多個書籤建立獨立的 PDF。 splitByChapters.submit=分割 PDF #File Chooser @@ -1893,12 +1947,12 @@ editTableOfContents.replaceExisting=取代現有書籤 (取消勾選以附加到 editTableOfContents.editorTitle=書籤編輯器 editTableOfContents.editorDesc=在下方新增和排列書籤。點選 + 新增子書籤。 editTableOfContents.addBookmark=新增書籤 -editTableOfContents.importBookmarksDefault=Import -editTableOfContents.importBookmarksFromJsonFile=Upload JSON file -editTableOfContents.importBookmarksFromClipboard=Paste from clipboard -editTableOfContents.exportBookmarksDefault=Export -editTableOfContents.exportBookmarksAsJson=Download as JSON -editTableOfContents.exportBookmarksAsText=Copy as text +editTableOfContents.importBookmarksDefault=匯入 +editTableOfContents.importBookmarksFromJsonFile=上傳 JSON 檔案 +editTableOfContents.importBookmarksFromClipboard=從剪貼簿貼上 +editTableOfContents.exportBookmarksDefault=匯出 +editTableOfContents.exportBookmarksAsJson=下載為 JSON +editTableOfContents.exportBookmarksAsText=複製為文字 editTableOfContents.desc.1=此工具可讓您在 PDF 文件中新增或編輯目錄 (書籤)。 editTableOfContents.desc.2=您可以透過將子書籤新增至父書籤來建立階層式結構。 editTableOfContents.desc.3=每個書籤都需要標題和目標頁碼。 diff --git a/app/core/src/main/resources/settings.yml.template b/app/core/src/main/resources/settings.yml.template index 78eb55b60..5f8ca51be 100644 --- a/app/core/src/main/resources/settings.yml.template +++ b/app/core/src/main/resources/settings.yml.template @@ -171,6 +171,8 @@ system: cleanupIntervalMinutes: 30 # How often to run cleanup (in minutes) startupCleanup: true # Clean up old temp files on startup cleanupSystemTemp: false # Whether to clean broader system temp directory + databaseBackup: + cron: '0 0 0 * * ?' # Cron expression for automatic database backups "0 0 0 * * ?" daily at midnight ui: appNameNavbar: '' # name displayed on the navigation bar diff --git a/app/core/src/main/resources/static/3rdPartyLicenses.json b/app/core/src/main/resources/static/3rdPartyLicenses.json index 062818603..7a9d9eeb5 100644 --- a/app/core/src/main/resources/static/3rdPartyLicenses.json +++ b/app/core/src/main/resources/static/3rdPartyLicenses.json @@ -24,7 +24,7 @@ { "moduleName": "com.bucket4j:bucket4j_jdk17-core", "moduleUrl": "http://github.com/bucket4j/bucket4j/bucket4j_jdk17-core", - "moduleVersion": "8.14.0", + "moduleVersion": "8.15.0", "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0" }, @@ -504,7 +504,7 @@ { "moduleName": "com.zaxxer:HikariCP", "moduleUrl": "https://github.com/brettwooldridge/HikariCP", - "moduleVersion": "6.3.1", + "moduleVersion": "6.3.2", "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" }, @@ -536,6 +536,13 @@ "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" }, + { + "moduleName": "commons-io:commons-io", + "moduleUrl": "https://commons.apache.org/proper/commons-io/", + "moduleVersion": "2.19.0", + "moduleLicense": "Apache-2.0", + "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" + }, { "moduleName": "commons-io:commons-io", "moduleUrl": "https://commons.apache.org/proper/commons-io/", @@ -559,56 +566,56 @@ { "moduleName": "io.jsonwebtoken:jjwt-api", "moduleUrl": "https://github.com/jwtk/jjwt", - "moduleVersion": "0.12.6", - "moduleLicense": "Apache License, Version 2.0", + "moduleVersion": "0.13.0", + "moduleLicense": "Apache-2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "io.jsonwebtoken:jjwt-impl", "moduleUrl": "https://github.com/jwtk/jjwt", - "moduleVersion": "0.12.6", - "moduleLicense": "Apache License, Version 2.0", + "moduleVersion": "0.13.0", + "moduleLicense": "Apache-2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "io.jsonwebtoken:jjwt-jackson", "moduleUrl": "https://github.com/jwtk/jjwt", - "moduleVersion": "0.12.6", - "moduleLicense": "Apache License, Version 2.0", + "moduleVersion": "0.13.0", + "moduleLicense": "Apache-2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "io.micrometer:micrometer-commons", "moduleUrl": "https://github.com/micrometer-metrics/micrometer", - "moduleVersion": "1.15.2", + "moduleVersion": "1.15.3", "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" }, { "moduleName": "io.micrometer:micrometer-core", "moduleUrl": "https://github.com/micrometer-metrics/micrometer", - "moduleVersion": "1.15.2", + "moduleVersion": "1.15.3", "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" }, { "moduleName": "io.micrometer:micrometer-jakarta9", "moduleUrl": "https://github.com/micrometer-metrics/micrometer", - "moduleVersion": "1.15.2", + "moduleVersion": "1.15.3", "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" }, { "moduleName": "io.micrometer:micrometer-observation", "moduleUrl": "https://github.com/micrometer-metrics/micrometer", - "moduleVersion": "1.15.2", + "moduleVersion": "1.15.3", "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" }, { "moduleName": "io.micrometer:micrometer-registry-prometheus", "moduleUrl": "https://github.com/micrometer-metrics/micrometer", - "moduleVersion": "1.15.2", + "moduleVersion": "1.15.3", "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" }, @@ -657,21 +664,21 @@ { "moduleName": "io.swagger.core.v3:swagger-annotations-jakarta", "moduleUrl": "https://github.com/swagger-api/swagger-core/modules/swagger-annotations", - "moduleVersion": "2.2.35", + "moduleVersion": "2.2.36", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "io.swagger.core.v3:swagger-core-jakarta", "moduleUrl": "https://github.com/swagger-api/swagger-core/modules/swagger-core", - "moduleVersion": "2.2.35", + "moduleVersion": "2.2.36", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "io.swagger.core.v3:swagger-models-jakarta", "moduleUrl": "https://github.com/swagger-api/swagger-core/modules/swagger-models", - "moduleVersion": "2.2.35", + "moduleVersion": "2.2.36", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, @@ -724,6 +731,13 @@ "moduleLicense": "GPL2 w/ CPE", "moduleLicenseUrl": "https://www.gnu.org/software/classpath/license.html" }, + { + "moduleName": "jakarta.mail:jakarta.mail-api", + "moduleUrl": "https://www.eclipse.org", + "moduleVersion": "2.1.4", + "moduleLicense": "GPL2 w/ CPE", + "moduleLicenseUrl": "https://www.gnu.org/software/classpath/license.html" + }, { "moduleName": "jakarta.persistence:jakarta.persistence-api", "moduleUrl": "https://www.eclipse.org", @@ -738,6 +752,13 @@ "moduleLicense": "GPL2 w/ CPE", "moduleLicenseUrl": "https://www.gnu.org/software/classpath/license.html" }, + { + "moduleName": "jakarta.servlet:jakarta.servlet-api", + "moduleUrl": "https://www.eclipse.org", + "moduleVersion": "6.1.0", + "moduleLicense": "GPL2 w/ CPE", + "moduleLicenseUrl": "https://www.gnu.org/software/classpath/license.html" + }, { "moduleName": "jakarta.transaction:jakarta.transaction-api", "moduleUrl": "https://projects.eclipse.org/projects/ee4j.jta", @@ -803,7 +824,7 @@ }, { "moduleName": "net.bytebuddy:byte-buddy", - "moduleVersion": "1.17.6", + "moduleVersion": "1.17.7", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" }, @@ -862,6 +883,20 @@ "moduleLicense": "Apache-2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" }, + { + "moduleName": "org.apache.commons:commons-lang3", + "moduleUrl": "https://commons.apache.org/proper/commons-lang/", + "moduleVersion": "3.18.0", + "moduleLicense": "Apache-2.0", + "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" + }, + { + "moduleName": "org.apache.commons:commons-text", + "moduleUrl": "https://commons.apache.org/proper/commons-text", + "moduleVersion": "1.10.0", + "moduleLicense": "Apache License, Version 2.0", + "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" + }, { "moduleName": "org.apache.commons:commons-text", "moduleUrl": "https://commons.apache.org/proper/commons-text", @@ -946,7 +981,7 @@ { "moduleName": "org.apache.tomcat.embed:tomcat-embed-el", "moduleUrl": "https://tomcat.apache.org/", - "moduleVersion": "10.1.43", + "moduleVersion": "10.1.44", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" }, @@ -984,6 +1019,13 @@ "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" }, + { + "moduleName": "org.bouncycastle:bcpkix-jdk18on", + "moduleUrl": "https://www.bouncycastle.org/java.html", + "moduleVersion": "1.72", + "moduleLicense": "Bouncy Castle Licence", + "moduleLicenseUrl": "https://www.bouncycastle.org/licence.html" + }, { "moduleName": "org.bouncycastle:bcpkix-jdk18on", "moduleUrl": "https://www.bouncycastle.org/download/bouncy-castle-java/", @@ -998,6 +1040,13 @@ "moduleLicense": "Bouncy Castle Licence", "moduleLicenseUrl": "https://www.bouncycastle.org/licence.html" }, + { + "moduleName": "org.bouncycastle:bcutil-jdk18on", + "moduleUrl": "https://www.bouncycastle.org/java.html", + "moduleVersion": "1.72", + "moduleLicense": "Bouncy Castle Licence", + "moduleLicenseUrl": "https://www.bouncycastle.org/licence.html" + }, { "moduleName": "org.bouncycastle:bcutil-jdk18on", "moduleUrl": "https://www.bouncycastle.org/download/bouncy-castle-java/", @@ -1041,196 +1090,196 @@ { "moduleName": "org.eclipse.angus:angus-mail", "moduleUrl": "https://www.eclipse.org", - "moduleVersion": "2.0.3", + "moduleVersion": "2.0.4", "moduleLicense": "GPL2 w/ CPE", "moduleLicenseUrl": "https://www.gnu.org/software/classpath/license.html" }, { "moduleName": "org.eclipse.angus:jakarta.mail", "moduleUrl": "https://www.eclipse.org", - "moduleVersion": "2.0.3", + "moduleVersion": "2.0.4", "moduleLicense": "GPL2 w/ CPE", "moduleLicenseUrl": "https://www.gnu.org/software/classpath/license.html" }, { "moduleName": "org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jakarta-client", "moduleUrl": "https://jetty.org/", - "moduleVersion": "12.0.23", + "moduleVersion": "12.0.25", "moduleLicense": "Eclipse Public License - Version 2.0", "moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/" }, { "moduleName": "org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jakarta-common", "moduleUrl": "https://jetty.org/", - "moduleVersion": "12.0.23", + "moduleVersion": "12.0.25", "moduleLicense": "Eclipse Public License - Version 2.0", "moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/" }, { "moduleName": "org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jakarta-server", "moduleUrl": "https://jetty.org/", - "moduleVersion": "12.0.23", + "moduleVersion": "12.0.25", "moduleLicense": "Eclipse Public License - Version 2.0", "moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/" }, { "moduleName": "org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jetty-server", "moduleUrl": "https://jetty.org/", - "moduleVersion": "12.0.23", + "moduleVersion": "12.0.25", "moduleLicense": "Eclipse Public License - Version 2.0", "moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/" }, { "moduleName": "org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-servlet", "moduleUrl": "https://jetty.org/", - "moduleVersion": "12.0.23", + "moduleVersion": "12.0.25", "moduleLicense": "Eclipse Public License - Version 2.0", "moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/" }, { "moduleName": "org.eclipse.jetty.ee10:jetty-ee10-annotations", "moduleUrl": "https://jetty.org/", - "moduleVersion": "12.0.23", + "moduleVersion": "12.0.25", "moduleLicense": "Eclipse Public License - Version 2.0", "moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/" }, { "moduleName": "org.eclipse.jetty.ee10:jetty-ee10-plus", "moduleUrl": "https://jetty.org/", - "moduleVersion": "12.0.23", + "moduleVersion": "12.0.25", "moduleLicense": "Eclipse Public License - Version 2.0", "moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/" }, { "moduleName": "org.eclipse.jetty.ee10:jetty-ee10-servlet", "moduleUrl": "https://jetty.org/", - "moduleVersion": "12.0.23", + "moduleVersion": "12.0.25", "moduleLicense": "Eclipse Public License - Version 2.0", "moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/" }, { "moduleName": "org.eclipse.jetty.ee10:jetty-ee10-servlets", "moduleUrl": "https://jetty.org/", - "moduleVersion": "12.0.23", + "moduleVersion": "12.0.25", "moduleLicense": "Eclipse Public License - Version 2.0", "moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/" }, { "moduleName": "org.eclipse.jetty.ee10:jetty-ee10-webapp", "moduleUrl": "https://jetty.org/", - "moduleVersion": "12.0.23", + "moduleVersion": "12.0.25", "moduleLicense": "Eclipse Public License - Version 2.0", "moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/" }, { "moduleName": "org.eclipse.jetty.websocket:jetty-websocket-core-client", "moduleUrl": "https://jetty.org/", - "moduleVersion": "12.0.23", + "moduleVersion": "12.0.25", "moduleLicense": "Eclipse Public License - Version 2.0", "moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/" }, { "moduleName": "org.eclipse.jetty.websocket:jetty-websocket-core-common", "moduleUrl": "https://jetty.org/", - "moduleVersion": "12.0.23", + "moduleVersion": "12.0.25", "moduleLicense": "Eclipse Public License - Version 2.0", "moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/" }, { "moduleName": "org.eclipse.jetty.websocket:jetty-websocket-core-server", "moduleUrl": "https://jetty.org/", - "moduleVersion": "12.0.23", + "moduleVersion": "12.0.25", "moduleLicense": "Eclipse Public License - Version 2.0", "moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/" }, { "moduleName": "org.eclipse.jetty.websocket:jetty-websocket-jetty-api", "moduleUrl": "https://jetty.org/", - "moduleVersion": "12.0.23", + "moduleVersion": "12.0.25", "moduleLicense": "Eclipse Public License - Version 2.0", "moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/" }, { "moduleName": "org.eclipse.jetty.websocket:jetty-websocket-jetty-common", "moduleUrl": "https://jetty.org/", - "moduleVersion": "12.0.23", + "moduleVersion": "12.0.25", "moduleLicense": "Eclipse Public License - Version 2.0", "moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/" }, { "moduleName": "org.eclipse.jetty:jetty-alpn-client", "moduleUrl": "https://jetty.org/", - "moduleVersion": "12.0.23", + "moduleVersion": "12.0.25", "moduleLicense": "Eclipse Public License - Version 2.0", "moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/" }, { "moduleName": "org.eclipse.jetty:jetty-client", "moduleUrl": "https://jetty.org/", - "moduleVersion": "12.0.23", + "moduleVersion": "12.0.25", "moduleLicense": "Eclipse Public License - Version 2.0", "moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/" }, { "moduleName": "org.eclipse.jetty:jetty-ee", "moduleUrl": "https://jetty.org/", - "moduleVersion": "12.0.23", + "moduleVersion": "12.0.25", "moduleLicense": "Eclipse Public License - Version 2.0", "moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/" }, { "moduleName": "org.eclipse.jetty:jetty-http", "moduleUrl": "https://jetty.org/", - "moduleVersion": "12.0.23", + "moduleVersion": "12.0.25", "moduleLicense": "Eclipse Public License - Version 2.0", "moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/" }, { "moduleName": "org.eclipse.jetty:jetty-io", "moduleUrl": "https://jetty.org/", - "moduleVersion": "12.0.23", + "moduleVersion": "12.0.25", "moduleLicense": "Eclipse Public License - Version 2.0", "moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/" }, { "moduleName": "org.eclipse.jetty:jetty-plus", "moduleUrl": "https://jetty.org/", - "moduleVersion": "12.0.23", + "moduleVersion": "12.0.25", "moduleLicense": "Eclipse Public License - Version 2.0", "moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/" }, { "moduleName": "org.eclipse.jetty:jetty-security", "moduleUrl": "https://jetty.org/", - "moduleVersion": "12.0.23", + "moduleVersion": "12.0.25", "moduleLicense": "Eclipse Public License - Version 2.0", "moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/" }, { "moduleName": "org.eclipse.jetty:jetty-server", "moduleUrl": "https://jetty.org/", - "moduleVersion": "12.0.23", + "moduleVersion": "12.0.25", "moduleLicense": "Eclipse Public License - Version 2.0", "moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/" }, { "moduleName": "org.eclipse.jetty:jetty-session", "moduleUrl": "https://jetty.org/", - "moduleVersion": "12.0.23", + "moduleVersion": "12.0.25", "moduleLicense": "Eclipse Public License - Version 2.0", "moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/" }, { "moduleName": "org.eclipse.jetty:jetty-util", "moduleUrl": "https://jetty.org/", - "moduleVersion": "12.0.23", + "moduleVersion": "12.0.25", "moduleLicense": "Eclipse Public License - Version 2.0", "moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/" }, { "moduleName": "org.eclipse.jetty:jetty-xml", "moduleUrl": "https://jetty.org/", - "moduleVersion": "12.0.23", + "moduleVersion": "12.0.25", "moduleLicense": "Eclipse Public License - Version 2.0", "moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/" }, @@ -1272,15 +1321,16 @@ { "moduleName": "org.hibernate.orm:hibernate-core", "moduleUrl": "https://www.hibernate.org/orm/6.6", - "moduleVersion": "6.6.22.Final", + "moduleVersion": "6.6.26.Final", "moduleLicense": "GNU Library General Public License v2.1 or later", "moduleLicenseUrl": "https://www.opensource.org/licenses/LGPL-2.1" }, { "moduleName": "org.hibernate.validator:hibernate-validator", - "moduleVersion": "8.0.2.Final", + "moduleUrl": "https://hibernate.org/validator", + "moduleVersion": "8.0.3.Final", "moduleLicense": "Apache License 2.0", - "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" + "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" }, { "moduleName": "org.jboss.logging:jboss-logging", @@ -1470,320 +1520,327 @@ }, { "moduleName": "org.springdoc:springdoc-openapi-starter-common", - "moduleVersion": "2.8.9", + "moduleVersion": "2.8.12", "moduleLicense": "The Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" }, { "moduleName": "org.springdoc:springdoc-openapi-starter-webmvc-api", - "moduleVersion": "2.8.9", + "moduleVersion": "2.8.12", "moduleLicense": "The Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" }, { "moduleName": "org.springdoc:springdoc-openapi-starter-webmvc-ui", - "moduleVersion": "2.8.9", + "moduleVersion": "2.8.12", "moduleLicense": "The Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" }, { "moduleName": "org.springframework.boot:spring-boot", "moduleUrl": "https://spring.io/projects/spring-boot", - "moduleVersion": "3.5.4", + "moduleVersion": "3.5.5", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.boot:spring-boot-actuator", "moduleUrl": "https://spring.io/projects/spring-boot", - "moduleVersion": "3.5.4", + "moduleVersion": "3.5.5", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.boot:spring-boot-actuator-autoconfigure", "moduleUrl": "https://spring.io/projects/spring-boot", - "moduleVersion": "3.5.4", + "moduleVersion": "3.5.5", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.boot:spring-boot-autoconfigure", "moduleUrl": "https://spring.io/projects/spring-boot", - "moduleVersion": "3.5.4", + "moduleVersion": "3.5.5", + "moduleLicense": "Apache License, Version 2.0", + "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" + }, + { + "moduleName": "org.springframework.boot:spring-boot-devtools", + "moduleUrl": "https://spring.io/projects/spring-boot", + "moduleVersion": "3.5.5", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.boot:spring-boot-starter", "moduleUrl": "https://spring.io/projects/spring-boot", - "moduleVersion": "3.5.4", + "moduleVersion": "3.5.5", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.boot:spring-boot-starter-actuator", "moduleUrl": "https://spring.io/projects/spring-boot", - "moduleVersion": "3.5.4", + "moduleVersion": "3.5.5", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.boot:spring-boot-starter-aop", "moduleUrl": "https://spring.io/projects/spring-boot", - "moduleVersion": "3.5.4", + "moduleVersion": "3.5.5", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.boot:spring-boot-starter-cache", "moduleUrl": "https://spring.io/projects/spring-boot", - "moduleVersion": "3.5.4", + "moduleVersion": "3.5.5", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.boot:spring-boot-starter-data-jpa", "moduleUrl": "https://spring.io/projects/spring-boot", - "moduleVersion": "3.5.4", + "moduleVersion": "3.5.5", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.boot:spring-boot-starter-jdbc", "moduleUrl": "https://spring.io/projects/spring-boot", - "moduleVersion": "3.5.4", + "moduleVersion": "3.5.5", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.boot:spring-boot-starter-jetty", "moduleUrl": "https://spring.io/projects/spring-boot", - "moduleVersion": "3.5.4", + "moduleVersion": "3.5.5", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.boot:spring-boot-starter-json", "moduleUrl": "https://spring.io/projects/spring-boot", - "moduleVersion": "3.5.4", + "moduleVersion": "3.5.5", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.boot:spring-boot-starter-logging", "moduleUrl": "https://spring.io/projects/spring-boot", - "moduleVersion": "3.5.4", + "moduleVersion": "3.5.5", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.boot:spring-boot-starter-mail", "moduleUrl": "https://spring.io/projects/spring-boot", - "moduleVersion": "3.5.4", + "moduleVersion": "3.5.5", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.boot:spring-boot-starter-oauth2-client", "moduleUrl": "https://spring.io/projects/spring-boot", - "moduleVersion": "3.5.4", + "moduleVersion": "3.5.5", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.boot:spring-boot-starter-security", "moduleUrl": "https://spring.io/projects/spring-boot", - "moduleVersion": "3.5.4", + "moduleVersion": "3.5.5", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.boot:spring-boot-starter-thymeleaf", "moduleUrl": "https://spring.io/projects/spring-boot", - "moduleVersion": "3.5.4", + "moduleVersion": "3.5.5", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.boot:spring-boot-starter-validation", "moduleUrl": "https://spring.io/projects/spring-boot", - "moduleVersion": "3.5.4", + "moduleVersion": "3.5.5", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.boot:spring-boot-starter-web", "moduleUrl": "https://spring.io/projects/spring-boot", - "moduleVersion": "3.5.4", + "moduleVersion": "3.5.5", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.data:spring-data-commons", "moduleUrl": "https://spring.io/projects/spring-data", - "moduleVersion": "3.5.2", + "moduleVersion": "3.5.3", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.data:spring-data-jpa", "moduleUrl": "https://projects.spring.io/spring-data-jpa", - "moduleVersion": "3.5.2", + "moduleVersion": "3.5.3", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.security:spring-security-config", "moduleUrl": "https://spring.io/projects/spring-security", - "moduleVersion": "6.5.2", + "moduleVersion": "6.5.3", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.security:spring-security-core", "moduleUrl": "https://spring.io/projects/spring-security", - "moduleVersion": "6.5.2", + "moduleVersion": "6.5.3", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.security:spring-security-crypto", "moduleUrl": "https://spring.io/projects/spring-security", - "moduleVersion": "6.5.2", + "moduleVersion": "6.5.3", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.security:spring-security-oauth2-client", "moduleUrl": "https://spring.io/projects/spring-security", - "moduleVersion": "6.5.2", + "moduleVersion": "6.5.3", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.security:spring-security-oauth2-core", "moduleUrl": "https://spring.io/projects/spring-security", - "moduleVersion": "6.5.2", + "moduleVersion": "6.5.3", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.security:spring-security-oauth2-jose", "moduleUrl": "https://spring.io/projects/spring-security", - "moduleVersion": "6.5.2", + "moduleVersion": "6.5.3", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.security:spring-security-saml2-service-provider", "moduleUrl": "https://spring.io/projects/spring-security", - "moduleVersion": "6.5.2", + "moduleVersion": "6.5.3", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.security:spring-security-web", "moduleUrl": "https://spring.io/projects/spring-security", - "moduleVersion": "6.5.2", + "moduleVersion": "6.5.3", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.session:spring-session-core", "moduleUrl": "https://spring.io/projects/spring-session", - "moduleVersion": "3.5.1", + "moduleVersion": "3.5.2", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework:spring-aop", "moduleUrl": "https://github.com/spring-projects/spring-framework", - "moduleVersion": "6.2.9", + "moduleVersion": "6.2.10", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework:spring-aspects", "moduleUrl": "https://github.com/spring-projects/spring-framework", - "moduleVersion": "6.2.9", + "moduleVersion": "6.2.10", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework:spring-beans", "moduleUrl": "https://github.com/spring-projects/spring-framework", - "moduleVersion": "6.2.9", + "moduleVersion": "6.2.10", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework:spring-context", "moduleUrl": "https://github.com/spring-projects/spring-framework", - "moduleVersion": "6.2.9", + "moduleVersion": "6.2.10", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework:spring-context-support", "moduleUrl": "https://github.com/spring-projects/spring-framework", - "moduleVersion": "6.2.9", + "moduleVersion": "6.2.10", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework:spring-core", "moduleUrl": "https://github.com/spring-projects/spring-framework", - "moduleVersion": "6.2.9", + "moduleVersion": "6.2.10", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework:spring-expression", "moduleUrl": "https://github.com/spring-projects/spring-framework", - "moduleVersion": "6.2.9", + "moduleVersion": "6.2.10", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework:spring-jcl", "moduleUrl": "https://github.com/spring-projects/spring-framework", - "moduleVersion": "6.2.9", + "moduleVersion": "6.2.10", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework:spring-jdbc", "moduleUrl": "https://github.com/spring-projects/spring-framework", - "moduleVersion": "6.2.9", + "moduleVersion": "6.2.10", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework:spring-orm", "moduleUrl": "https://github.com/spring-projects/spring-framework", - "moduleVersion": "6.2.9", + "moduleVersion": "6.2.10", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework:spring-tx", "moduleUrl": "https://github.com/spring-projects/spring-framework", - "moduleVersion": "6.2.9", + "moduleVersion": "6.2.10", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework:spring-web", "moduleUrl": "https://github.com/spring-projects/spring-framework", - "moduleVersion": "6.2.9", + "moduleVersion": "6.2.10", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework:spring-webmvc", "moduleUrl": "https://github.com/spring-projects/spring-framework", - "moduleVersion": "6.2.9", + "moduleVersion": "6.2.10", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, @@ -1821,7 +1878,7 @@ { "moduleName": "org.webjars:swagger-ui", "moduleUrl": "https://www.webjars.org", - "moduleVersion": "5.21.0", + "moduleVersion": "5.28.0", "moduleLicense": "Apache-2.0" }, { diff --git a/app/core/src/main/resources/static/css/sign.css b/app/core/src/main/resources/static/css/sign.css index 39ea4c9be..0b1524e07 100644 --- a/app/core/src/main/resources/static/css/sign.css +++ b/app/core/src/main/resources/static/css/sign.css @@ -10,6 +10,22 @@ select#font-select option { text-align: center; } +.signature-color-picker { + display: inline-flex; + align-items: center; + gap: 0.5rem; + margin: 0.5rem 0 0.75rem; +} + +.signature-color-picker input[type="color"] { + width: 42px; + height: 32px; + padding: 0; + border: 1px solid var(--bs-border-color, #ced4da); + border-radius: 6px; + background: transparent; +} + #drawing-pad-canvas { background: rgba(125, 125, 125, 0.2); width: 100%; diff --git a/app/core/src/main/resources/static/js/DecryptFiles.js b/app/core/src/main/resources/static/js/DecryptFiles.js index 0e5b58a92..e569d8839 100644 --- a/app/core/src/main/resources/static/js/DecryptFiles.js +++ b/app/core/src/main/resources/static/js/DecryptFiles.js @@ -128,11 +128,15 @@ export class DecryptFile { (error.message && error.message.includes('Invalid PDF structure'))) { // Handle corrupted PDF files console.error('Corrupted PDF detected:', error); - this.showErrorBanner( - `${window.stirlingPDF.pdfCorruptedMessage.replace('{0}', file.name)}`, - error.stack || '', - `${window.stirlingPDF.tryRepairMessage}` - ); + if (window.stirlingPDF.currentPage !== 'repair') { + this.showErrorBanner( + `${window.stirlingPDF.pdfCorruptedMessage.replace('{0}', file.name)}`, + error.stack || '', + `${window.stirlingPDF.tryRepairMessage}` + ); + } else { + console.log('Suppressing corrupted PDF warning banner on repair page'); + } throw new Error('PDF file is corrupted.'); } diff --git a/app/core/src/main/resources/static/js/downloader.js b/app/core/src/main/resources/static/js/downloader.js index b5324dd82..9e074be5e 100644 --- a/app/core/src/main/resources/static/js/downloader.js +++ b/app/core/src/main/resources/static/js/downloader.js @@ -249,10 +249,15 @@ (error.message && error.message.includes('Invalid PDF structure'))) { // Handle corrupted PDF files console.log(`Corrupted PDF detected: ${file.name}`, error); - showErrorBanner( - `${window.stirlingPDF.pdfCorruptedMessage.replace('{0}', file.name)}`, - `${window.stirlingPDF.tryRepairMessage}` - ); + if (window.stirlingPDF.currentPage !== 'repair') { + showErrorBanner( + `${window.stirlingPDF.pdfCorruptedMessage.replace('{0}', file.name)}`, + `${window.stirlingPDF.tryRepairMessage}` + ); + } else { + // On repair page, suppress banner; user already knows and is repairing + console.log('Suppressing corrupted PDF banner on repair page'); + } throw error; } else { console.log(`Error loading PDF: ${file.name}`, error); @@ -482,6 +487,11 @@ } progressBar.css('width', '100%'); progressBar.attr('aria-valuenow', Array.from(files).length); + setTimeout(() => { + progressBar.closest('.progressBarContainer').hide(); + progressBar.css('width', '0%'); + progressBar.attr('aria-valuenow', 0); + }, 1000); } function updateProgressBar(progressBar, files) { diff --git a/app/core/src/main/resources/static/js/merge.js b/app/core/src/main/resources/static/js/merge.js index 9aa7c0113..01d7d97d9 100644 --- a/app/core/src/main/resources/static/js/merge.js +++ b/app/core/src/main/resources/static/js/merge.js @@ -175,6 +175,13 @@ function updateFiles() { } } document.getElementById("fileInput-input").files = dataTransfer.files; + + // Also populate hidden fileOrder to preserve visible order + const order = Array.from(liElements) + .map((li) => li.querySelector(".filename").innerText) + .join("\n"); + const orderInput = document.getElementById("fileOrder"); + if (orderInput) orderInput.value = order; } document.querySelector("#resetFileInputBtn").addEventListener("click", ()=>{ diff --git a/app/core/src/main/resources/static/js/multitool/PdfContainer.js b/app/core/src/main/resources/static/js/multitool/PdfContainer.js index 125801a0a..ade68f0d9 100644 --- a/app/core/src/main/resources/static/js/multitool/PdfContainer.js +++ b/app/core/src/main/resources/static/js/multitool/PdfContainer.js @@ -694,7 +694,7 @@ class PdfContainer { const documentBlob = new Blob([pdfBytesArray[i]], { type: 'application/pdf', }); - zip.file(baseNameString + '-' + (i + 1) + '.pdf', documentBlob); + zip.file(baseNameString + '-' + String(i + 1).padStart(1 + Math.floor(Math.log10(pdfBytesArray.length)), "0") + '.pdf', documentBlob); } return zip; diff --git a/app/core/src/main/resources/static/js/navbar.js b/app/core/src/main/resources/static/js/navbar.js index 1fd46ed70..be9d2df12 100644 --- a/app/core/src/main/resources/static/js/navbar.js +++ b/app/core/src/main/resources/static/js/navbar.js @@ -75,39 +75,46 @@ function setupDropdowns() { }); } -window.tooltipSetup = () => { - const tooltipElements = document.querySelectorAll('[title]'); +function tooltipSetup() { + // initialize global tooltip element or get reference + let customTooltip = document.getElementById("customTooltip"); + if (!customTooltip) { + customTooltip = document.createElement("div"); + customTooltip.id = "customTooltip"; + customTooltip.className = "btn-tooltip"; + document.body.appendChild(customTooltip); + } + + function updateTooltipPosition(event, text) { + if (window.innerWidth >= 1200) { + customTooltip.textContent = text; + customTooltip.style.display = "block"; + customTooltip.style.left = `${event.pageX + 10}px`; + customTooltip.style.top = `${event.pageY + 10}px`; + } + } + + function hideTooltip() { + customTooltip.style.display = "none"; + } + + // find uninitialized tooltips and set up event listeners + const tooltipElements = document.querySelectorAll("[title]"); tooltipElements.forEach((element) => { - const tooltipText = element.getAttribute('title'); - element.removeAttribute('title'); - element.setAttribute('data-title', tooltipText); - const customTooltip = document.createElement('div'); - customTooltip.className = 'btn-tooltip'; - customTooltip.textContent = tooltipText; + const tooltipText = element.getAttribute("title"); + element.removeAttribute("title"); + element.setAttribute("data-title", tooltipText); // no UI effect, just for reference - document.body.appendChild(customTooltip); + element.addEventListener("mouseenter", (event) => updateTooltipPosition(event, tooltipText)); + element.addEventListener("mousemove", (event) => updateTooltipPosition(event, tooltipText)); + element.addEventListener("mouseleave", hideTooltip); - element.addEventListener('mouseenter', (event) => { - if (window.innerWidth >= 1200) { - customTooltip.style.display = 'block'; - customTooltip.style.left = `${event.pageX + 10}px`; - customTooltip.style.top = `${event.pageY + 10}px`; - } - }); - - element.addEventListener('mousemove', (event) => { - if (window.innerWidth >= 1200) { - customTooltip.style.left = `${event.pageX + 10}px`; - customTooltip.style.top = `${event.pageY + 10}px`; - } - }); - - element.addEventListener('mouseleave', () => { - customTooltip.style.display = 'none'; - }); + // in case UI moves and mouseleave is not triggered, tooltip is readded when mouse is moved over the element + element.addEventListener("click", hideTooltip); }); }; +window.tooltipSetup = tooltipSetup; // Override the bootstrap dropdown styles for mobile function fixNavbarDropdownStyles() { diff --git a/app/core/src/main/resources/static/js/pages/edit-table-of-contents.js b/app/core/src/main/resources/static/js/pages/edit-table-of-contents.js index 82c92a50e..9b8cb5cdd 100644 --- a/app/core/src/main/resources/static/js/pages/edit-table-of-contents.js +++ b/app/core/src/main/resources/static/js/pages/edit-table-of-contents.js @@ -316,9 +316,7 @@ document.addEventListener("DOMContentLoaded", function () { updateBookmarkData(); // Initialize tooltips for dynamically added elements - if (typeof $ !== "undefined") { - $('[data-bs-toggle="tooltip"]').tooltip(); - } + window.tooltipSetup(); } // Create the main bookmark element with collapsible interface @@ -390,8 +388,6 @@ document.addEventListener("DOMContentLoaded", function () { childCount.style.fontSize = "0.7rem"; childCount.style.padding = "0.2em 0.5em"; childCount.textContent = bookmark.children.length; - childCount.setAttribute("data-bs-toggle", "tooltip"); - childCount.setAttribute("data-bs-placement", "top"); childCount.title = `${bookmark.children.length} child bookmark${bookmark.children.length > 1 ? "s" : ""}`; toggleContainer.appendChild(childCount); } else { @@ -577,10 +573,6 @@ document.addEventListener("DOMContentLoaded", function () { button.type = "button"; button.className = `btn ${className} btn-bookmark-action`; button.innerHTML = `${icon}`; - - // Use Bootstrap tooltips - button.setAttribute("data-bs-toggle", "tooltip"); - button.setAttribute("data-bs-placement", "top"); button.title = title; button.addEventListener("click", clickHandler); @@ -601,10 +593,6 @@ document.addEventListener("DOMContentLoaded", function () { // Update the add bookmark button appearance with clear visual cue addBookmarkBtn.innerHTML = 'add Add Top-level Bookmark'; addBookmarkBtn.className = "btn btn-primary btn-add-bookmark top-level"; - - // Use Bootstrap tooltips - addBookmarkBtn.setAttribute("data-bs-toggle", "tooltip"); - addBookmarkBtn.setAttribute("data-bs-placement", "top"); addBookmarkBtn.title = "Add a new top-level bookmark"; // Add icon to empty state button as well @@ -612,14 +600,10 @@ document.addEventListener("DOMContentLoaded", function () { const emptyStateBtn = document.querySelector(".btn-add-first-bookmark"); if (emptyStateBtn) { emptyStateBtn.innerHTML = 'add Add First Bookmark'; - emptyStateBtn.setAttribute("data-bs-toggle", "tooltip"); - emptyStateBtn.setAttribute("data-bs-placement", "top"); emptyStateBtn.title = "Add first bookmark"; // Initialize tooltips for the empty state button - if (typeof $ !== "undefined") { - $('[data-bs-toggle="tooltip"]').tooltip(); - } + window.tooltipSetup(); } }; diff --git a/app/core/src/main/resources/static/js/pages/sign.js b/app/core/src/main/resources/static/js/pages/sign.js index 36902aa7b..ec02e75b3 100644 --- a/app/core/src/main/resources/static/js/pages/sign.js +++ b/app/core/src/main/resources/static/js/pages/sign.js @@ -7,6 +7,12 @@ window.goToFirstOrLastPage = goToFirstOrLastPage; let currentPreviewSrc = null; +function getSelectedSignatureColor() { + const textPicker = document.getElementById('signature-color-text'); + const drawPicker = document.getElementById('signature-color'); + return (textPicker && textPicker.value) || (drawPicker && drawPicker.value) || '#000000'; +} + function toggleSignatureView() { const gridView = document.getElementById("gridView"); const listView = document.getElementById("listView"); @@ -242,9 +248,19 @@ const signaturePadCanvas = document.getElementById("drawing-pad-canvas"); const signaturePad = new SignaturePad(signaturePadCanvas, { minWidth: 1, maxWidth: 2, - penColor: "black", + penColor: "#000000", }); +// Keep pad color in sync if draw picker exists +(function initPadColorSync() { + const drawPicker = document.getElementById('signature-color'); + if (!drawPicker) return; + if (drawPicker.value) signaturePad.penColor = drawPicker.value; + drawPicker.addEventListener('input', () => { + signaturePad.penColor = drawPicker.value || '#000000'; + }); +})(); + function addDraggableFromPad() { if (signaturePad.isEmpty()) return; const startTime = Date.now(); @@ -328,6 +344,7 @@ function addDraggableFromText() { const sigText = document.getElementById("sigText").value; const font = document.querySelector("select[name=font]").value; const fontSize = 100; + const color = getSelectedSignatureColor(); const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); @@ -340,6 +357,7 @@ function addDraggableFromText() { canvas.width = textWidth; canvas.height = paragraphs.length * textHeight * 1.35; // for tails ctx.font = `${fontSize}px ${font}`; + ctx.fillStyle = color; ctx.textBaseline = "top"; diff --git a/app/core/src/main/resources/static/js/sign/signature-canvas.js b/app/core/src/main/resources/static/js/sign/signature-canvas.js index a3f7b1e90..042236d69 100644 --- a/app/core/src/main/resources/static/js/sign/signature-canvas.js +++ b/app/core/src/main/resources/static/js/sign/signature-canvas.js @@ -4,9 +4,21 @@ const redoButton = document.getElementById("signature-redo-button"); const signaturePad = new SignaturePad(signaturePadCanvas, { minWidth: 1, maxWidth: 2, - penColor: 'black', + penColor: '#000000', // default color }); +(function initSignatureColor() { + const colorInput = document.getElementById('signature-color'); + if (!colorInput) return; + + if (colorInput.value) { + signaturePad.penColor = colorInput.value; + } + colorInput.addEventListener('input', () => { + signaturePad.penColor = colorInput.value || '#000000'; + }); +})(); + let undoData = []; signaturePad.addEventListener("endStroke", () => { @@ -16,10 +28,10 @@ signaturePad.addEventListener("endStroke", () => { window.addEventListener("keydown", (event) => { switch (true) { case event.key === "z" && event.ctrlKey: - undoButton.click(); + undoButton?.click(); break; case event.key === "y" && event.ctrlKey: - redoButton.click(); + redoButton?.click(); break; } }); diff --git a/app/core/src/main/resources/templates/adminSettings.html b/app/core/src/main/resources/templates/adminSettings.html index 4c2a7988b..1c4fca184 100644 --- a/app/core/src/main/resources/templates/adminSettings.html +++ b/app/core/src/main/resources/templates/adminSettings.html @@ -302,8 +302,8 @@
- - + +
diff --git a/app/core/src/main/resources/templates/auto-split-pdf.html b/app/core/src/main/resources/templates/auto-split-pdf.html index 25584918c..b52aa0009 100644 --- a/app/core/src/main/resources/templates/auto-split-pdf.html +++ b/app/core/src/main/resources/templates/auto-split-pdf.html @@ -29,7 +29,7 @@ th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}">
- +

diff --git a/app/core/src/main/resources/templates/convert/cbr-to-pdf.html b/app/core/src/main/resources/templates/convert/cbr-to-pdf.html new file mode 100644 index 000000000..b49c06f81 --- /dev/null +++ b/app/core/src/main/resources/templates/convert/cbr-to-pdf.html @@ -0,0 +1,44 @@ + + + + + + + + + +

+
+ +

+
+
+
+
+ auto_stories + +
+
+
+
+ +
+ + +
+ +
+ +
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/app/core/src/main/resources/templates/convert/cbz-to-pdf.html b/app/core/src/main/resources/templates/convert/cbz-to-pdf.html new file mode 100644 index 000000000..c7e547cdb --- /dev/null +++ b/app/core/src/main/resources/templates/convert/cbz-to-pdf.html @@ -0,0 +1,44 @@ + + + + + + + + + +
+
+ +

+
+
+
+
+ auto_stories + +
+
+
+
+ +
+ + +
+ +
+ +
+
+
+
+
+ +
+ + + diff --git a/app/core/src/main/resources/templates/convert/eml-to-pdf.html b/app/core/src/main/resources/templates/convert/eml-to-pdf.html index e3b469ab9..37e412018 100644 --- a/app/core/src/main/resources/templates/convert/eml-to-pdf.html +++ b/app/core/src/main/resources/templates/convert/eml-to-pdf.html @@ -25,16 +25,16 @@ th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='.eml,message/rfc822')}"> -
- - +
+ +
-
-
- - +
+
+ +
diff --git a/app/core/src/main/resources/templates/convert/img-to-pdf.html b/app/core/src/main/resources/templates/convert/img-to-pdf.html index c3b01eec2..c0638c55f 100644 --- a/app/core/src/main/resources/templates/convert/img-to-pdf.html +++ b/app/core/src/main/resources/templates/convert/img-to-pdf.html @@ -36,7 +36,7 @@
- +
diff --git a/app/core/src/main/resources/templates/convert/pdf-to-cbr.html b/app/core/src/main/resources/templates/convert/pdf-to-cbr.html new file mode 100644 index 000000000..3594bd301 --- /dev/null +++ b/app/core/src/main/resources/templates/convert/pdf-to-cbr.html @@ -0,0 +1,45 @@ + + + + + + + + + +
+
+ +

+
+
+
+
+ auto_stories + +
+
+
+
+ +
+ + +
Higher DPI results in better quality but larger file size.
+
+ +
+ +
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/app/core/src/main/resources/templates/convert/pdf-to-cbz.html b/app/core/src/main/resources/templates/convert/pdf-to-cbz.html new file mode 100644 index 000000000..fd1023a9c --- /dev/null +++ b/app/core/src/main/resources/templates/convert/pdf-to-cbz.html @@ -0,0 +1,42 @@ + + + + + + + + + +
+
+ +

+
+
+
+
+ auto_stories + +
+
+
+
+
+ + +
+
+ +
+
+
+
+
+ +
+ + + diff --git a/app/core/src/main/resources/templates/convert/pdf-to-img.html b/app/core/src/main/resources/templates/convert/pdf-to-img.html index 78c7ca901..531d9dad9 100644 --- a/app/core/src/main/resources/templates/convert/pdf-to-img.html +++ b/app/core/src/main/resources/templates/convert/pdf-to-img.html @@ -73,6 +73,10 @@
+
+ + +
diff --git a/app/core/src/main/resources/templates/convert/url-to-pdf.html b/app/core/src/main/resources/templates/convert/url-to-pdf.html index 67e89c101..231ad8c3c 100644 --- a/app/core/src/main/resources/templates/convert/url-to-pdf.html +++ b/app/core/src/main/resources/templates/convert/url-to-pdf.html @@ -19,6 +19,7 @@ link
+


diff --git a/app/core/src/main/resources/templates/crop.html b/app/core/src/main/resources/templates/crop.html index 0617bf9b6..e91c481c3 100644 --- a/app/core/src/main/resources/templates/crop.html +++ b/app/core/src/main/resources/templates/crop.html @@ -22,6 +22,7 @@ +
diff --git a/app/core/src/main/resources/templates/edit-table-of-contents.html b/app/core/src/main/resources/templates/edit-table-of-contents.html index 21a7204e7..503ab99f4 100644 --- a/app/core/src/main/resources/templates/edit-table-of-contents.html +++ b/app/core/src/main/resources/templates/edit-table-of-contents.html @@ -33,13 +33,12 @@ th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}">
-
+
- diff --git a/app/core/src/main/resources/templates/fragments/navElements.html b/app/core/src/main/resources/templates/fragments/navElements.html index 19d6536ce..1441a1969 100644 --- a/app/core/src/main/resources/templates/fragments/navElements.html +++ b/app/core/src/main/resources/templates/fragments/navElements.html @@ -47,6 +47,12 @@
+
+
+
+
@@ -71,6 +77,12 @@
+
+
+
+
@@ -98,13 +110,19 @@
-
+
- +
- +
diff --git a/app/core/src/main/resources/templates/home.html b/app/core/src/main/resources/templates/home.html index 334939a2c..9773d54ab 100644 --- a/app/core/src/main/resources/templates/home.html +++ b/app/core/src/main/resources/templates/home.html @@ -152,7 +152,7 @@
- +
diff --git a/app/core/src/main/resources/templates/merge-pdfs.html b/app/core/src/main/resources/templates/merge-pdfs.html index f8d194a4e..794eac805 100644 --- a/app/core/src/main/resources/templates/merge-pdfs.html +++ b/app/core/src/main/resources/templates/merge-pdfs.html @@ -21,19 +21,20 @@
+
-
- +
+
-
- +
+
diff --git a/app/core/src/main/resources/templates/misc/add-page-numbers.html b/app/core/src/main/resources/templates/misc/add-page-numbers.html index 186253b69..c85d0404e 100644 --- a/app/core/src/main/resources/templates/misc/add-page-numbers.html +++ b/app/core/src/main/resources/templates/misc/add-page-numbers.html @@ -99,6 +99,24 @@
+
+ +
+ +
+ +
diff --git a/app/core/src/main/resources/templates/misc/change-metadata.html b/app/core/src/main/resources/templates/misc/change-metadata.html index 524fde9c8..265ea298d 100644 --- a/app/core/src/main/resources/templates/misc/change-metadata.html +++ b/app/core/src/main/resources/templates/misc/change-metadata.html @@ -19,56 +19,56 @@

-
- +
+
-
- +
+
- +
- +
- +
- +
- +
- +
- +
- +
- +
- -
diff --git a/app/core/src/main/resources/templates/misc/extract-images.html b/app/core/src/main/resources/templates/misc/extract-images.html index d47a72d7c..afcc2a789 100644 --- a/app/core/src/main/resources/templates/misc/extract-images.html +++ b/app/core/src/main/resources/templates/misc/extract-images.html @@ -27,8 +27,8 @@
-
- +
+
diff --git a/app/core/src/main/resources/templates/misc/flatten.html b/app/core/src/main/resources/templates/misc/flatten.html index 51f06737d..fca06e093 100644 --- a/app/core/src/main/resources/templates/misc/flatten.html +++ b/app/core/src/main/resources/templates/misc/flatten.html @@ -22,7 +22,7 @@
- +

diff --git a/app/core/src/main/resources/templates/misc/ocr-pdf.html b/app/core/src/main/resources/templates/misc/ocr-pdf.html index e1de37eb8..6fa85b1fc 100644 --- a/app/core/src/main/resources/templates/misc/ocr-pdf.html +++ b/app/core/src/main/resources/templates/misc/ocr-pdf.html @@ -55,7 +55,7 @@
- +
@@ -82,24 +82,24 @@
- - + +
- - + +
- - + +
- - + +
- - + +

diff --git a/app/core/src/main/resources/templates/misc/replace-color.html b/app/core/src/main/resources/templates/misc/replace-color.html index 4defcff72..18b8c9f0c 100644 --- a/app/core/src/main/resources/templates/misc/replace-color.html +++ b/app/core/src/main/resources/templates/misc/replace-color.html @@ -30,6 +30,7 @@ +
@@ -56,6 +57,18 @@
+ + @@ -74,12 +87,15 @@ $('#high-contrast-options').hide(); $('#custom-color-1').hide(); $('#custom-color-2').hide(); + $('#color-space-info').hide(); if (selectedOption === "HIGH_CONTRAST_COLOR") { $('#high-contrast-options').show(); } else if (selectedOption === "CUSTOM_COLOR") { $('#custom-color-1').show(); $('#custom-color-2').show(); + } else if (selectedOption === "COLOR_SPACE_CONVERSION") { + $('#color-space-info').show(); } }); }); diff --git a/app/core/src/main/resources/templates/misc/scanner-effect.html b/app/core/src/main/resources/templates/misc/scanner-effect.html index e04e82f19..b2900816f 100644 --- a/app/core/src/main/resources/templates/misc/scanner-effect.html +++ b/app/core/src/main/resources/templates/misc/scanner-effect.html @@ -40,8 +40,8 @@
- - + +