Compare commits

..
1 Commits
Author SHA1 Message Date
Anthony Stirling b0b9eee5c8 selfisgned 2026-01-30 16:49:59 +00:00
121 changed files with 2342 additions and 5304 deletions
+2 -2
View File
@@ -59,8 +59,8 @@ def find_duplicate_keys(file_path, keys=None, prefix=""):
return duplicates
# Maximum size for TOML files (e.g., 1 MB)
MAX_FILE_SIZE = 1000 * 1024
# Maximum size for TOML files (e.g., 570 KB)
MAX_FILE_SIZE = 570 * 1024
def parse_toml_file(file_path):
+329 -280
View File
@@ -97,9 +97,9 @@ cffi==2.0.0 \
--hash=sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453 \
--hash=sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf
# via weasyprint
cfgv==3.5.0 \
--hash=sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0 \
--hash=sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132
cfgv==3.4.0 \
--hash=sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9 \
--hash=sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560
# via pre-commit
cssselect2==0.8.0 \
--hash=sha256:46fc70ebc41ced7a32cd42d58b1884d72ade23d21e5a4eaaf022401c13f0e76e \
@@ -109,269 +109,259 @@ distlib==0.4.0 \
--hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \
--hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d
# via virtualenv
filelock==3.20.3 \
--hash=sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1 \
--hash=sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1
filelock==3.20.0 \
--hash=sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2 \
--hash=sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4
# via virtualenv
fonttools==4.61.1 \
--hash=sha256:0de30bfe7745c0d1ffa2b0b7048fb7123ad0d71107e10ee090fa0b16b9452e87 \
--hash=sha256:10d88e55330e092940584774ee5e8a6971b01fc2f4d3466a1d6c158230880796 \
--hash=sha256:11f35ad7805edba3aac1a3710d104592df59f4b957e30108ae0ba6c10b11dd75 \
--hash=sha256:15acc09befd16a0fb8a8f62bc147e1a82817542d72184acca9ce6e0aeda9fa6d \
--hash=sha256:17d2bf5d541add43822bcf0c43d7d847b160c9bb01d15d5007d84e2217aaa371 \
--hash=sha256:2180f14c141d2f0f3da43f3a81bc8aa4684860f6b0e6f9e165a4831f24e6a23b \
--hash=sha256:21e7c8d76f62ab13c9472ccf74515ca5b9a761d1bde3265152a6dc58700d895b \
--hash=sha256:41a7170d042e8c0024703ed13b71893519a1a6d6e18e933e3ec7507a2c26a4b2 \
--hash=sha256:41ed4b5ec103bd306bb68f81dc166e77409e5209443e5773cb4ed837bcc9b0d3 \
--hash=sha256:497c31ce314219888c0e2fce5ad9178ca83fe5230b01a5006726cdf3ac9f24d9 \
--hash=sha256:4c1b526c8d3f615a7b1867f38a9410849c8f4aef078535742198e942fba0e9bd \
--hash=sha256:4d7092bb38c53bbc78e9255a59158b150bcdc115a1e3b3ce0b5f267dc35dd63c \
--hash=sha256:4f5686e1fe5fce75d82d93c47a438a25bf0d1319d2843a926f741140b2b16e0c \
--hash=sha256:58b0ee0ab5b1fc9921eccfe11d1435added19d6494dde14e323f25ad2bc30c56 \
--hash=sha256:5ce02f38a754f207f2f06557523cd39a06438ba3aafc0639c477ac409fc64e37 \
--hash=sha256:5fade934607a523614726119164ff621e8c30e8fa1ffffbbd358662056ba69f0 \
--hash=sha256:5fe9fd43882620017add5eabb781ebfbc6998ee49b35bd7f8f79af1f9f99a958 \
--hash=sha256:64102ca87e84261419c3747a0d20f396eb024bdbeb04c2bfb37e2891f5fadcb5 \
--hash=sha256:664c5a68ec406f6b1547946683008576ef8b38275608e1cee6c061828171c118 \
--hash=sha256:6675329885c44657f826ef01d9e4fb33b9158e9d93c537d84ad8399539bc6f69 \
--hash=sha256:75c1a6dfac6abd407634420c93864a1e274ebc1c7531346d9254c0d8f6ca00f9 \
--hash=sha256:75da8f28eff26defba42c52986de97b22106cb8f26515b7c22443ebc9c2d3261 \
--hash=sha256:77efb033d8d7ff233385f30c62c7c79271c8885d5c9657d967ede124671bbdfb \
--hash=sha256:78a7d3ab09dc47ac1a363a493e6112d8cabed7ba7caad5f54dbe2f08676d1b47 \
--hash=sha256:7c7db70d57e5e1089a274cbb2b1fd635c9a24de809a231b154965d415d6c6d24 \
--hash=sha256:8c56c488ab471628ff3bfa80964372fc13504ece601e0d97a78ee74126b2045c \
--hash=sha256:91669ccac46bbc1d09e9273546181919064e8df73488ea087dcac3e2968df9ba \
--hash=sha256:9b666a475a65f4e839d3d10473fad6d47e0a9db14a2f4a224029c5bfde58ad2c \
--hash=sha256:9cfef3ab326780c04d6646f68d4b4742aae222e8b8ea1d627c74e38afcbc9d91 \
--hash=sha256:a13fc8aeb24bad755eea8f7f9d409438eb94e82cf86b08fe77a03fbc8f6a96b1 \
--hash=sha256:a75c301f96db737e1c5ed5fd7d77d9c34466de16095a266509e13da09751bd19 \
--hash=sha256:a76d4cb80f41ba94a6691264be76435e5f72f2cb3cab0b092a6212855f71c2f6 \
--hash=sha256:aed04cabe26f30c1647ef0e8fbb207516fd40fe9472e9439695f5c6998e60ac5 \
--hash=sha256:b148b56f5de675ee16d45e769e69f87623a4944f7443850bf9a9376e628a89d2 \
--hash=sha256:b501c862d4901792adaec7c25b1ecc749e2662543f68bb194c42ba18d6eec98d \
--hash=sha256:b846a1fcf8beadeb9ea4f44ec5bdde393e2f1569e17d700bfc49cd69bde75881 \
--hash=sha256:b931ae8f62db78861b0ff1ac017851764602288575d65b8e8ff1963fed419063 \
--hash=sha256:c33ab3ca9d3ccd581d58e989d67554e42d8d4ded94ab3ade3508455fe70e65f7 \
--hash=sha256:c6604b735bb12fef8e0efd5578c9fb5d3d8532d5001ea13a19cddf295673ee09 \
--hash=sha256:d8db08051fc9e7d8bc622f2112511b8107d8f27cd89e2f64ec45e9825e8288da \
--hash=sha256:d9203500f7c63545b4ce3799319fe4d9feb1a1b89b28d3cb5abd11b9dd64147e \
--hash=sha256:dc492779501fa723b04d0ab1f5be046797fee17d27700476edc7ee9ae535a61e \
--hash=sha256:e6bcdf33aec38d16508ce61fd81838f24c83c90a1d1b8c68982857038673d6b8 \
--hash=sha256:e76ce097e3c57c4bcb67c5aa24a0ecdbd9f74ea9219997a707a4061fbe2707aa \
--hash=sha256:eff1ac3cc66c2ac7cda1e64b4e2f3ffef474b7335f92fc3833fc632d595fcee6 \
--hash=sha256:f3cb4a569029b9f291f88aafc927dd53683757e640081ca8c412781ea144565e \
--hash=sha256:f79b168428351d11e10c5aeb61a74e1851ec221081299f4cf56036a95431c43a \
--hash=sha256:fa646ecec9528bef693415c79a86e733c70a4965dd938e9a226b0fc64c9d2e6c \
--hash=sha256:fe2efccb324948a11dd09d22136fe2ac8a97d6c1347cf0b58a911dcd529f66b7 \
--hash=sha256:fff4f534200a04b4a36e7ae3cb74493afe807b517a09e99cb4faa89a34ed6ecd
fonttools==4.60.1 \
--hash=sha256:022beaea4b73a70295b688f817ddc24ed3e3418b5036ffcd5658141184ef0d0c \
--hash=sha256:026290e4ec76583881763fac284aca67365e0be9f13a7fb137257096114cb3bc \
--hash=sha256:0b0835ed15dd5b40d726bb61c846a688f5b4ce2208ec68779bc81860adb5851a \
--hash=sha256:0eae96373e4b7c9e45d099d7a523444e3554360927225c1cdae221a58a45b856 \
--hash=sha256:122e1a8ada290423c493491d002f622b1992b1ab0b488c68e31c413390dc7eb2 \
--hash=sha256:1410155d0e764a4615774e5c2c6fc516259fe3eca5882f034eb9bfdbee056259 \
--hash=sha256:145daa14bf24824b677b9357c5e44fd8895c2a8f53596e1b9ea3496081dc692c \
--hash=sha256:1525796c3ffe27bb6268ed2a1bb0dcf214d561dfaf04728abf01489eb5339dce \
--hash=sha256:154cb6ee417e417bf5f7c42fe25858c9140c26f647c7347c06f0cc2d47eff003 \
--hash=sha256:2299df884c11162617a66b7c316957d74a18e3758c0274762d2cc87df7bc0272 \
--hash=sha256:2409d5fb7b55fd70f715e6d34e7a6e4f7511b8ad29a49d6df225ee76da76dd77 \
--hash=sha256:268ecda8ca6cb5c4f044b1fb9b3b376e8cd1b361cef275082429dc4174907038 \
--hash=sha256:282dafa55f9659e8999110bd8ed422ebe1c8aecd0dc396550b038e6c9a08b8ea \
--hash=sha256:2ee06fc57512144d8b0445194c2da9f190f61ad51e230f14836286470c99f854 \
--hash=sha256:3630e86c484263eaac71d117085d509cbcf7b18f677906824e4bace598fb70d2 \
--hash=sha256:398447f3d8c0c786cbf1209711e79080a40761eb44b27cdafffb48f52bcec258 \
--hash=sha256:4ba4bd646e86de16160f0fb72e31c3b9b7d0721c3e5b26b9fa2fc931dfdb2652 \
--hash=sha256:5664fd1a9ea7f244487ac8f10340c4e37664675e8667d6fee420766e0fb3cf08 \
--hash=sha256:583b7f8e3c49486e4d489ad1deacfb8d5be54a8ef34d6df824f6a171f8511d99 \
--hash=sha256:596ecaca36367027d525b3b426d8a8208169d09edcf8c7506aceb3a38bfb55c7 \
--hash=sha256:5c1015318e4fec75dd4943ad5f6a206d9727adf97410d58b7e32ab644a807914 \
--hash=sha256:66929e2ea2810c6533a5184f938502cfdaea4bc3efb7130d8cc02e1c1b4108d6 \
--hash=sha256:6ec722ee589e89a89f5b7574f5c45604030aa6ae24cb2c751e2707193b466fed \
--hash=sha256:6f68576bb4bbf6060c7ab047b1574a1ebe5c50a17de62830079967b211059ebb \
--hash=sha256:7473a8ed9ed09aeaa191301244a5a9dbe46fe0bf54f9d6cd21d83044c3321217 \
--hash=sha256:7b0c6d57ab00dae9529f3faf187f2254ea0aa1e04215cf2f1a8ec277c96661bc \
--hash=sha256:7b4c32e232a71f63a5d00259ca3d88345ce2a43295bb049d21061f338124246f \
--hash=sha256:8177ec9676ea6e1793c8a084a90b65a9f778771998eb919d05db6d4b1c0b114c \
--hash=sha256:839565cbf14645952d933853e8ade66a463684ed6ed6c9345d0faf1f0e868877 \
--hash=sha256:875cb7764708b3132637f6c5fb385b16eeba0f7ac9fa45a69d35e09b47045801 \
--hash=sha256:8a44788d9d91df72d1a5eac49b31aeb887a5f4aab761b4cffc4196c74907ea85 \
--hash=sha256:8b4eb332f9501cb1cd3d4d099374a1e1306783ff95489a1026bde9eb02ccc34a \
--hash=sha256:906306ac7afe2156fcf0042173d6ebbb05416af70f6b370967b47f8f00103bbb \
--hash=sha256:992775c9fbe2cf794786fa0ffca7f09f564ba3499b8fe9f2f80bd7197db60383 \
--hash=sha256:996a4d1834524adbb423385d5a629b868ef9d774670856c63c9a0408a3063401 \
--hash=sha256:9a52f254ce051e196b8fe2af4634c2d2f02c981756c6464dc192f1b6050b4e28 \
--hash=sha256:9d0ced62b59e0430b3690dbc5373df1c2aa7585e9a8ce38eff87f0fd993c5b01 \
--hash=sha256:a140761c4ff63d0cb9256ac752f230460ee225ccef4ad8f68affc723c88e2036 \
--hash=sha256:a184b2ea57b13680ab6d5fbde99ccef152c95c06746cb7718c583abd8f945ccc \
--hash=sha256:a3db56f153bd4c5c2b619ab02c5db5192e222150ce5a1bc10f16164714bc39ac \
--hash=sha256:a46b2f450bc79e06ef3b6394f0c68660529ed51692606ad7f953fc2e448bc903 \
--hash=sha256:a884aef09d45ba1206712c7dbda5829562d3fea7726935d3289d343232ecb0d3 \
--hash=sha256:b2cf105cee600d2de04ca3cfa1f74f1127f8455b71dbad02b9da6ec266e116d6 \
--hash=sha256:b33a7884fabd72bdf5f910d0cf46be50dce86a0362a65cfc746a4168c67eb96c \
--hash=sha256:b42d86938e8dda1cd9a1a87a6d82f1818eaf933348429653559a458d027446da \
--hash=sha256:b6379e7546ba4ae4b18f8ae2b9bc5960936007a1c0e30b342f662577e8bc3299 \
--hash=sha256:c7420a2696a44650120cdd269a5d2e56a477e2bfa9d95e86229059beb1c19e15 \
--hash=sha256:c8651e0d4b3bdeda6602b85fdc2abbefc1b41e573ecb37b6779c4ca50753a199 \
--hash=sha256:d066ea419f719ed87bc2c99a4a4bfd77c2e5949cb724588b9dd58f3fd90b92bf \
--hash=sha256:e6c58beb17380f7c2ea181ea11e7db8c0ceb474c9dd45f48e71e2cb577d146a1 \
--hash=sha256:e852d9dda9f93ad3651ae1e3bb770eac544ec93c3807888798eccddf84596537 \
--hash=sha256:ec3681a0cb34c255d76dd9d865a55f260164adb9fa02628415cdc2d43ee2c05d \
--hash=sha256:ee0c0b3b35b34f782afc673d503167157094a16f442ace7c6c5e0ca80b08f50c \
--hash=sha256:eedacb5c5d22b7097482fa834bda0dafa3d914a4e829ec83cdea2a01f8c813c4 \
--hash=sha256:ef00af0439ebfee806b25f24c8f92109157ff3fac5731dc7867957812e87b8d9 \
--hash=sha256:f0e8817c7d1a0c2eedebf57ef9a9896f3ea23324769a9a2061a80fe8852705ed \
--hash=sha256:f3d5be054c461d6a2268831f04091dc82753176f6ea06dc6047a5e168265a987 \
--hash=sha256:f4b5c37a5f40e4d733d3bbaaef082149bee5a5ea3156a785ff64d949bd1353fa
# via weasyprint
identify==2.6.16 \
--hash=sha256:391ee4d77741d994189522896270b787aed8670389bfd60f326d677d64a6dfb0 \
--hash=sha256:846857203b5511bbe94d5a352a48ef2359532bc8f6727b5544077a0dcfb24980
identify==2.6.15 \
--hash=sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757 \
--hash=sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf
# via pre-commit
nodeenv==1.10.0 \
--hash=sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827 \
--hash=sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb
nodeenv==1.9.1 \
--hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \
--hash=sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9
# via pre-commit
numpy==2.4.1 \
--hash=sha256:0093e85df2960d7e4049664b26afc58b03236e967fb942354deef3208857a04c \
--hash=sha256:09aa8a87e45b55a1c2c205d42e2808849ece5c484b2aab11fecabec3841cafba \
--hash=sha256:0cce2a669e3c8ba02ee563c7835f92c153cf02edff1ae05e1823f1dde21b16a5 \
--hash=sha256:0e6e8f9d9ecf95399982019c01223dc130542960a12edfa8edd1122dfa66a8a8 \
--hash=sha256:0f118ce6b972080ba0758c6087c3617b5ba243d806268623dc34216d69099ba0 \
--hash=sha256:178de8f87948163d98a4c9ab5bee4ce6519ca918926ec8df195af582de28544d \
--hash=sha256:18e14c4d09d55eef39a6ab5b08406e84bc6869c1e34eef45564804f90b7e0574 \
--hash=sha256:2023ef86243690c2791fd6353e5b4848eedaa88ca8a2d129f462049f6d484696 \
--hash=sha256:20d4649c773f66cc2fc36f663e091f57c3b7655f936a4c681b4250855d1da8f5 \
--hash=sha256:2302dc0224c1cbc49bb94f7064f3f923a971bfae45c33870dcbff63a2a550505 \
--hash=sha256:26f0bcd9c79a00e339565b303badc74d3ea2bd6d52191eeca5f95936cad107d0 \
--hash=sha256:297c72b1b98100c2e8f873d5d35fb551fce7040ade83d67dd51d38c8d42a2162 \
--hash=sha256:2f44de05659b67d20499cbc96d49f2650769afcb398b79b324bb6e297bfe3844 \
--hash=sha256:2ffd257026eb1b34352e749d7cc1678b5eeec3e329ad8c9965a797e08ccba205 \
--hash=sha256:382ad67d99ef49024f11d1ce5dcb5ad8432446e4246a4b014418ba3a1175a1f4 \
--hash=sha256:3869ea1ee1a1edc16c29bbe3a2f2a4e515cc3a44d43903ad41e0cacdbaf733dc \
--hash=sha256:3d1a100e48cb266090a031397863ff8a30050ceefd798f686ff92c67a486753d \
--hash=sha256:423797bdab2eeefbe608d7c1ec7b2b4fd3c58d51460f1ee26c7500a1d9c9ee93 \
--hash=sha256:42d7dd5fa36d16d52a84f821eb96031836fd405ee6955dd732f2023724d0aa01 \
--hash=sha256:49e792ec351315e16da54b543db06ca8a86985ab682602d90c60ef4ff4db2a9c \
--hash=sha256:4e53170557d37ae404bf8d542ca5b7c629d6efa1117dac6a83e394142ea0a43f \
--hash=sha256:4f1b68ff47680c2925f8063402a693ede215f0257f02596b1318ecdfb1d79e33 \
--hash=sha256:4f9c360ecef085e5841c539a9a12b883dff005fbd7ce46722f5e9cef52634d82 \
--hash=sha256:529050522e983e00a6c1c6b67411083630de8b57f65e853d7b03d9281b8694d2 \
--hash=sha256:52b5f61bdb323b566b528899cc7db2ba5d1015bda7ea811a8bcf3c89c331fa42 \
--hash=sha256:538bf4ec353709c765ff75ae616c34d3c3dca1a68312727e8f2676ea644f8509 \
--hash=sha256:5adf01965456a664fc727ed69cc71848f28d063217c63e1a0e200a118d5eec9a \
--hash=sha256:5b55aa56165b17aaf15520beb9cbd33c9039810e0d9643dd4379e44294c7303e \
--hash=sha256:5d558123217a83b2d1ba316b986e9248a1ed1971ad495963d555ccd75dcb1556 \
--hash=sha256:5de60946f14ebe15e713a6f22850c2372fa72f4ff9a432ab44aa90edcadaa65a \
--hash=sha256:62fea415f83ad8fdb6c20840578e5fbaf5ddd65e0ec6c3c47eda0f69da172510 \
--hash=sha256:6436cffb4f2bf26c974344439439c95e152c9a527013f26b3577be6c2ca64295 \
--hash=sha256:6461de5113088b399d655d45c3897fa188766415d0f568f175ab071c8873bd73 \
--hash=sha256:69e7419c9012c4aaf695109564e3387f1259f001b4326dfa55907b098af082d3 \
--hash=sha256:71abbea030f2cfc3092a0ff9f8c8fdefdc5e0bf7d9d9c99663538bb0ecdac0b9 \
--hash=sha256:7211b95ca365519d3596a1d8688a95874cc94219d417504d9ecb2df99fa7bfa8 \
--hash=sha256:727c6c3275ddefa0dc078524a85e064c057b4f4e71ca5ca29a19163c607be745 \
--hash=sha256:79e9e06c4c2379db47f3f6fc7a8652e7498251789bf8ff5bd43bf478ef314ca2 \
--hash=sha256:7ad270f438cbdd402c364980317fb6b117d9ec5e226fff5b4148dd9aa9fc6e02 \
--hash=sha256:7d5d7999df434a038d75a748275cd6c0094b0ecdb0837342b332a82defc4dc4d \
--hash=sha256:8097529164c0f3e32bb89412a0905d9100bf434d9692d9fc275e18dcf53c9344 \
--hash=sha256:82c55962006156aeef1629b953fd359064aa47e4d82cfc8e67f0918f7da3344f \
--hash=sha256:8361ea4220d763e54cff2fbe7d8c93526b744f7cd9ddab47afeff7e14e8503be \
--hash=sha256:899d2c18024984814ac7e83f8f49d8e8180e2fbe1b2e252f2e7f1d06bea92425 \
--hash=sha256:8ad35f20be147a204e28b6a0575fbf3540c5e5f802634d4258d55b1ff5facce1 \
--hash=sha256:8f085da926c0d491ffff3096f91078cc97ea67e7e6b65e490bc8dcda65663be2 \
--hash=sha256:9171a42fcad32dcf3fa86f0a4faa5e9f8facefdb276f54b8b390d90447cff4e2 \
--hash=sha256:92a0e65272fd60bfa0d9278e0484c2f52fe03b97aedc02b357f33fe752c52ffb \
--hash=sha256:941c2a93313d030f219f3a71fd3d91a728b82979a5e8034eb2e60d394a2b83f9 \
--hash=sha256:98b35775e03ab7f868908b524fc0a84d38932d8daf7b7e1c3c3a1b6c7a2c9f15 \
--hash=sha256:a1ceafc5042451a858231588a104093474c6a5c57dcc724841f5c888d237d690 \
--hash=sha256:a73044b752f5d34d4232f25f18160a1cc418ea4507f5f11e299d8ac36875f8a0 \
--hash=sha256:a7870e8c5fc11aef57d6fea4b4085e537a3a60ad2cdd14322ed531fdca68d261 \
--hash=sha256:a92f227dbcdc9e4c3e193add1a189a9909947d4f8504c576f4a732fd0b54240a \
--hash=sha256:ac08c63cb7779b85e9d5318e6c3518b424bc1f364ac4cb2c6136f12e5ff2dccc \
--hash=sha256:b6bcf39112e956594b3331316d90c90c90fb961e39696bda97b89462f5f3943f \
--hash=sha256:c0faba4a331195bfa96f93dd9dfaa10b2c7aa8cda3a02b7fd635e588fe821bf5 \
--hash=sha256:ce9ce141a505053b3c7bce3216071f3bf5c182b8b28930f14cd24d43932cd2df \
--hash=sha256:cf6470d91d34bf669f61d515499859fa7a4c2f7c36434afb70e82df7217933f9 \
--hash=sha256:d3703409aac693fa82c0aee023a1ae06a6e9d065dba10f5e8e80f642f1e9d0a2 \
--hash=sha256:d3e3087f53e2b4428766b54932644d148613c5a595150533ae7f00dab2f319a8 \
--hash=sha256:d3f8f0df9f4b8be57b3bf74a1d087fec68f927a2fab68231fdb442bf2c12e426 \
--hash=sha256:d797454e37570cfd61143b73b8debd623c3c0952959adb817dd310a483d58a1b \
--hash=sha256:e1a27bb1b2dee45a2a53f5ca6ff2d1a7f135287883a1689e930d44d1ff296c87 \
--hash=sha256:e3bd2cb07841166420d2fa7146c96ce00cb3410664cbc1a6be028e456c4ee220 \
--hash=sha256:e7b6b5e28bbd47b7532698e5db2fe1db693d84b58c254e4389d99a27bb9b8f6b \
--hash=sha256:e867df947d427cdd7a60e3e271729090b0f0df80f5f10ab7dd436f40811699c3 \
--hash=sha256:ea66d2b41ca4a1630aae5507ee0a71647d3124d1741980138aa8f28f44dac36e \
--hash=sha256:edee228f76ee2dab4579fad6f51f6a305de09d444280109e0f75df247ff21501 \
--hash=sha256:f0a90aba7d521e6954670550e561a4cb925713bd944445dbe9e729b71f6cabee \
--hash=sha256:f93bc6892fe7b0663e5ffa83b61aab510aacffd58c16e012bb9352d489d90cb7 \
--hash=sha256:fb1461c99de4d040666ca0444057b06541e5642f800b71c56e6ea92d6a853a0c
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.13.0.90 \
--hash=sha256:0e0c8c9f620802fddc4fa7f471a1d263c7b0dca16cd9e7e2f996bb8bd2128c0c \
--hash=sha256:12a28674f215542c9bf93338de1b5bffd76996d32da9acb9e739fdb9c8bbd738 \
--hash=sha256:32255203040dc98803be96362e13f9e4bce20146898222d2e5c242f80de50da5 \
--hash=sha256:96060fc57a1abb1144b0b8129e2ff3bfcdd0ccd8e8bd05bd85256ff4ed587d3b \
--hash=sha256:dbc1f4625e5af3a80ebdbd84380227c0f445228588f2521b11af47710caca1ba \
--hash=sha256:e13790342591557050157713af17a7435ac1b50c65282715093c9297fa045d8f \
--hash=sha256:eba38bc255d0b7d1969c5bcc90a060ca2b61a3403b613872c750bfa5dfe9e03b \
--hash=sha256:f46b17ea0aa7e4124ca6ad71143f89233ae9557f61d2326bcdb34329a1ddf9bd
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==12.1.0 \
--hash=sha256:00162e9ca6d22b7c3ee8e61faa3c3253cd19b6a37f126cad04f2f88b306f557d \
--hash=sha256:079af2fb0c599c2ec144ba2c02766d1b55498e373b3ac64687e43849fbbef5bc \
--hash=sha256:0b022eaaf709541b391ee069f0022ee5b36c709df71986e3f7be312e46f42c84 \
--hash=sha256:0c27407a2d1b96774cbc4a7594129cc027339fd800cd081e44497722ea1179de \
--hash=sha256:0ddedfaa8b5f0b4ffbc2fa87b556dc59f6bb4ecb14a53b33f9189713ae8053c0 \
--hash=sha256:0deedf2ea233722476b3a81e8cdfbad786f7adbed5d848469fa59fe52396e4ef \
--hash=sha256:0ed07dca4a8464bada6139ab38f5382f83e5f111698caf3191cb8dbf27d908b4 \
--hash=sha256:0fde7ec5538ab5095cc02df38ee99b0443ff0e1c847a045554cf5f9af1f4aa82 \
--hash=sha256:15c794d74303828eaa957ff8070846d0efe8c630901a1c753fdc63850e19ecd9 \
--hash=sha256:1a949604f73eb07a8adab38c4fe50791f9919344398bdc8ac6b307f755fc7030 \
--hash=sha256:1f345e7bc9d7f368887c712aa5054558bad44d2a301ddf9248599f4161abc7c0 \
--hash=sha256:1fcc52d86ce7a34fd17cb04e87cfdb164648a3662a6f20565910a99653d66c18 \
--hash=sha256:21e686a21078b0f9cb8c8a961d99e6a4ddb88e0fc5ea6e130172ddddc2e5221a \
--hash=sha256:2415373395a831f53933c23ce051021e79c8cd7979822d8cc478547a3f4da8ef \
--hash=sha256:277518bf4fe74aa91489e1b20577473b19ee70fb97c374aa50830b279f25841b \
--hash=sha256:27b9baecb428899db6c0de572d6d305cfaf38ca1596b5c0542a5182e3e74e8c6 \
--hash=sha256:29a4cef9cb672363926f0470afc516dbf7305a14d8c54f7abbb5c199cd8f8179 \
--hash=sha256:3413c2ae377550f5487991d444428f1a8ae92784aac79caa8b1e3b89b175f77e \
--hash=sha256:351889afef0f485b84078ea40fe33727a0492b9af3904661b0abbafee0355b72 \
--hash=sha256:3ffaa2f0659e2f740473bcf03c702c39a8d4b2b7ffc629052028764324842c64 \
--hash=sha256:40a8e3b9e8773876d6e30daed22f016509e3987bab61b3b7fe309d7019a87451 \
--hash=sha256:414b9a78e14ffeb98128863314e62c3f24b8a86081066625700b7985b3f529bd \
--hash=sha256:43aca0a55ce1eefc0aefa6253661cb54571857b1a7b2964bd8a1e3ef4b729924 \
--hash=sha256:43b4899cfd091a9693a1278c4982f3e50f7fb7cff5153b05174b4afc9593b616 \
--hash=sha256:461f9dfdafa394c59cd6d818bdfdbab4028b83b02caadaff0ffd433faf4c9a7a \
--hash=sha256:4f9f6a650743f0ddee5593ac9e954ba1bdbc5e150bc066586d4f26127853ab94 \
--hash=sha256:53d8b764726d3af1a138dd353116f774e3862ec7e3794e0c8781e30db0f35dfc \
--hash=sha256:565c986f4b45c020f5421a4cea13ef294dde9509a8577f29b2fc5edc7587fff8 \
--hash=sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9 \
--hash=sha256:5cb7bc1966d031aec37ddb9dcf15c2da5b2e9f7cc3ca7c54473a20a927e1eb91 \
--hash=sha256:5da841d81b1a05ef940a8567da92decaa15bc4d7dedb540a8c219ad83d91808a \
--hash=sha256:5fee4c04aad8932da9f8f710af2c1a15a83582cfb884152a9caa79d4efcdbf9c \
--hash=sha256:609e89d9f90b581c8d16358c9087df76024cf058fa693dd3e1e1620823f39670 \
--hash=sha256:6258f3260986990ba2fa8a874f8b6e808cf5abb51a94015ca3dc3c68aa4f30ea \
--hash=sha256:64efdf00c09e31efd754448a383ea241f55a994fd079866b92d2bbff598aad91 \
--hash=sha256:65b80c1ee7e14a87d6a068dd3b0aea268ffcabfe0498d38661b00c5b4b22e74c \
--hash=sha256:6741e6f3074a35e47c77b23a4e4f2d90db3ed905cb1c5e6e0d49bff2045632bc \
--hash=sha256:681088909d7e8fa9e31b9799aaa59ba5234c58e5e4f1951b4c4d1082a2e980e0 \
--hash=sha256:6b7a9d1db5dad90e2991645874f708e87d9a3c370c243c2d7684d28f7e133e6b \
--hash=sha256:7315f9137087c4e0ee73a761b163fc9aa3b19f5f606a7fc08d83fd3e4379af65 \
--hash=sha256:742aea052cf5ab5034a53c3846165bc3ce88d7c38e954120db0ab867ca242661 \
--hash=sha256:75af0b4c229ac519b155028fa1be632d812a519abba9b46b20e50c6caa184f19 \
--hash=sha256:7b5dd7cbae20285cdb597b10eb5a2c13aa9de6cde9bb64a3c1317427b1db1ae1 \
--hash=sha256:7d6daa89a00b58c37cb1747ec9fb7ac3bc5ffd5949f5888657dfddde6d1312e0 \
--hash=sha256:800429ac32c9b72909c671aaf17ecd13110f823ddb7db4dfef412a5587c2c24e \
--hash=sha256:806f3987ffe10e867bab0ddad45df1148a2b98221798457fa097ad85d6e8bc75 \
--hash=sha256:808b99604f7873c800c4840f55ff389936ef1948e4e87645eaf3fccbc8477ac4 \
--hash=sha256:80941e6d573197a0c28f394753de529bb436b1ca990ed6e765cf42426abc39f8 \
--hash=sha256:84cabc7095dd535ca934d57e9ce2a72ffd216e435a84acb06b2277b1de2689bd \
--hash=sha256:8637e29d13f478bc4f153d8daa9ffb16455f0a6cb287da1b432fdad2bfbd66c7 \
--hash=sha256:896866d2d436563fa2a43a9d72f417874f16b5545955c54a64941e87c1376c61 \
--hash=sha256:8e178e3e99d3c0ea8fc64b88447f7cac8ccf058af422a6cedc690d0eadd98c51 \
--hash=sha256:907bfa8a9cb790748a9aa4513e37c88c59660da3bcfffbd24a7d9e6abf224551 \
--hash=sha256:9212d6b86917a2300669511ed094a9406888362e085f2431a7da985a6b124f45 \
--hash=sha256:92a7fe4225365c5e3a8e598982269c6d6698d3e783b3b1ae979e7819f9cd55c1 \
--hash=sha256:935b9d1aed48fcfb3f838caac506f38e29621b44ccc4f8a64d575cb1b2a88644 \
--hash=sha256:97e9993d5ed946aba26baf9c1e8cf18adbab584b99f452ee72f7ee8acb882796 \
--hash=sha256:983976c2ab753166dc66d36af6e8ec15bb511e4a25856e2227e5f7e00a160587 \
--hash=sha256:9f5fefaca968e700ad1a4a9de98bf0869a94e397fe3524c4c9450c1445252304 \
--hash=sha256:a332ac4ccb84b6dde65dbace8431f3af08874bf9770719d32a635c4ef411b18b \
--hash=sha256:a40905599d8079e09f25027423aed94f2823adaf2868940de991e53a449e14a8 \
--hash=sha256:a6dfc2af5b082b635af6e08e0d1f9f1c4e04d17d4e2ca0ef96131e85eda6eb17 \
--hash=sha256:a786bf667724d84aa29b5db1c61b7bfdde380202aaca12c3461afd6b71743171 \
--hash=sha256:a83e0850cb8f5ac975291ebfc4170ba481f41a28065277f7f735c202cd8e0af3 \
--hash=sha256:aa0c9cc0b82b14766a99fbe6084409972266e82f459821cd26997a488a7261a7 \
--hash=sha256:b17fbdbe01c196e7e159aacb889e091f28e61020a8abeac07b68079b6e626988 \
--hash=sha256:b63e13dd27da389ed9475b3d28510f0f954bca0041e8e551b2a4eb1eab56a39a \
--hash=sha256:b6e53e82ec2db0717eabb276aa56cf4e500c9a7cec2c2e189b55c24f65a3e8c0 \
--hash=sha256:bb0984b30e973f7e2884362b7d23d0a348c7143ee559f38ef3eaab640144204c \
--hash=sha256:bc11908616c8a283cf7d664f77411a5ed2a02009b0097ff8abbba5e79128ccf2 \
--hash=sha256:bdec5e43377761c5dbca620efb69a77f6855c5a379e32ac5b158f54c84212b14 \
--hash=sha256:bef9768cab184e7ae6e559c032e95ba8d07b3023c289f79a2bd36e8bf85605a5 \
--hash=sha256:c990547452ee2800d8506c4150280757f88532f3de2a58e3022e9b179107862a \
--hash=sha256:ca94b6aac0d7af2a10ba08c0f888b3d5114439b6b3ef39968378723622fed377 \
--hash=sha256:cad302dc10fac357d3467a74a9561c90609768a6f73a1923b0fd851b6486f8b0 \
--hash=sha256:d0a7735df32ccbcc98b98a1ac785cc4b19b580be1bdf0aeb5c03223220ea09d5 \
--hash=sha256:d70347c8a5b7ccd803ec0c85c8709f036e6348f1e6a5bf048ecd9c64d3550b8b \
--hash=sha256:d70534cea9e7966169ad29a903b99fc507e932069a881d0965a1a84bb57f6c6d \
--hash=sha256:db44d5c160a90df2d24a24760bbd37607d53da0b34fb546c4c232af7192298ac \
--hash=sha256:e115c15e3bc727b1ca3e641a909f77f8ca72a64fff150f666fcc85e57701c26c \
--hash=sha256:e2479c7f02f9d505682dc47df8c0ea1fc5e264c4d1629a5d63fe3e2334b89554 \
--hash=sha256:e5dcbe95016e88437ecf33544ba5db21ef1b8dd6e1b434a2cb2a3d605299e643 \
--hash=sha256:e6bdb408f7c9dd2a5ff2b14a3b0bb6d4deb29fb9961e6eb3ae2031ae9a5cec13 \
--hash=sha256:e75d3dba8fc1ddfec0cd752108f93b83b4f8d6ab40e524a95d35f016b9683b09 \
--hash=sha256:efdc140e7b63b8f739d09a99033aa430accce485ff78e6d311973a67b6bf3208 \
--hash=sha256:f10c98f49227ed8383d28174ee95155a675c4ed7f85e2e573b04414f7e371bda \
--hash=sha256:f188028b5af6b8fb2e9a76ac0f841a575bd1bd396e46ef0840d9b88a48fdbcea \
--hash=sha256:f188d580bd870cda1e15183790d1cc2fa78f666e76077d103edf048eed9c356e \
--hash=sha256:f45bd71d1fa5e5749587613037b172e0b3b23159d1c00ef2fc920da6f470e6f0 \
--hash=sha256:f61333d817698bdcdd0f9d7793e365ac3d2a21c1f1eb02b32ad6aefb8d8ea831 \
--hash=sha256:fb125d860738a09d363a88daa0f59c4533529a90e564785e20fe875b200b6dbd
pillow==12.0.0 \
--hash=sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643 \
--hash=sha256:09f2d0abef9e4e2f349305a4f8cc784a8a6c2f58a8c4892eea13b10a943bd26e \
--hash=sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e \
--hash=sha256:0fd00cac9c03256c8b2ff58f162ebcd2587ad3e1f2e397eab718c47e24d231cc \
--hash=sha256:110486b79f2d112cf6add83b28b627e369219388f64ef2f960fef9ebaf54c642 \
--hash=sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6 \
--hash=sha256:1ac11e8ea4f611c3c0147424eae514028b5e9077dd99ab91e1bd7bc33ff145e1 \
--hash=sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b \
--hash=sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399 \
--hash=sha256:21f241bdd5080a15bc86d3466a9f6074a9c2c2b314100dd896ac81ee6db2f1ba \
--hash=sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad \
--hash=sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47 \
--hash=sha256:27f95b12453d165099c84f8a8bfdfd46b9e4bda9e0e4b65f0635430027f55739 \
--hash=sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b \
--hash=sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f \
--hash=sha256:32ed80ea8a90ee3e6fa08c21e2e091bba6eda8eccc83dbc34c95169507a91f10 \
--hash=sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52 \
--hash=sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d \
--hash=sha256:3adfb466bbc544b926d50fe8f4a4e6abd8c6bffd28a26177594e6e9b2b76572b \
--hash=sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a \
--hash=sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9 \
--hash=sha256:455247ac8a4cfb7b9bc45b7e432d10421aea9fc2e74d285ba4072688a74c2e9d \
--hash=sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098 \
--hash=sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905 \
--hash=sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b \
--hash=sha256:5269cc1caeedb67e6f7269a42014f381f45e2e7cd42d834ede3c703a1d915fe3 \
--hash=sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371 \
--hash=sha256:55f818bd74fe2f11d4d7cbc65880a843c4075e0ac7226bc1a23261dbea531953 \
--hash=sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01 \
--hash=sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca \
--hash=sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e \
--hash=sha256:6ace95230bfb7cd79ef66caa064bbe2f2a1e63d93471c3a2e1f1348d9f22d6b7 \
--hash=sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27 \
--hash=sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082 \
--hash=sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e \
--hash=sha256:759de84a33be3b178a64c8ba28ad5c135900359e85fb662bc6e403ad4407791d \
--hash=sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8 \
--hash=sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a \
--hash=sha256:7dfb439562f234f7d57b1ac6bc8fe7f838a4bd49c79230e0f6a1da93e82f1fad \
--hash=sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3 \
--hash=sha256:805ebf596939e48dbb2e4922a1d3852cfc25c38160751ce02da93058b48d252a \
--hash=sha256:82240051c6ca513c616f7f9da06e871f61bfd7805f566275841af15015b8f98d \
--hash=sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353 \
--hash=sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee \
--hash=sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b \
--hash=sha256:90387104ee8400a7b4598253b4c406f8958f59fcf983a6cea2b50d59f7d63d0b \
--hash=sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a \
--hash=sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7 \
--hash=sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef \
--hash=sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a \
--hash=sha256:9fe611163f6303d1619bbcb653540a4d60f9e55e622d60a3108be0d5b441017a \
--hash=sha256:a3475b96f5908b3b16c47533daaa87380c491357d197564e0ba34ae75c0f3257 \
--hash=sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07 \
--hash=sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4 \
--hash=sha256:aa5129de4e174daccbc59d0a3b6d20eaf24417d59851c07ebb37aeb02947987c \
--hash=sha256:aeaefa96c768fc66818730b952a862235d68825c178f1b3ffd4efd7ad2edcb7c \
--hash=sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4 \
--hash=sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe \
--hash=sha256:b22bd8c974942477156be55a768f7aa37c46904c175be4e158b6a86e3a6b7ca8 \
--hash=sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5 \
--hash=sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6 \
--hash=sha256:b583dc9070312190192631373c6c8ed277254aa6e6084b74bdd0a6d3b221608e \
--hash=sha256:b87843e225e74576437fd5b6a4c2205d422754f84a06942cfaf1dc32243e45a8 \
--hash=sha256:bc91a56697869546d1b8f0a3ff35224557ae7f881050e99f615e0119bf934b4e \
--hash=sha256:bd87e140e45399c818fac4247880b9ce719e4783d767e030a883a970be632275 \
--hash=sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3 \
--hash=sha256:bdee52571a343d721fb2eb3b090a82d959ff37fc631e3f70422e0c2e029f3e76 \
--hash=sha256:bee2a6db3a7242ea309aa7ee8e2780726fed67ff4e5b40169f2c940e7eb09227 \
--hash=sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9 \
--hash=sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5 \
--hash=sha256:c607c90ba67533e1b2355b821fef6764d1dd2cbe26b8c1005ae84f7aea25ff79 \
--hash=sha256:c7b2a63fd6d5246349f3d3f37b14430d73ee7e8173154461785e43036ffa96ca \
--hash=sha256:c828a1ae702fc712978bda0320ba1b9893d99be0badf2647f693cc01cf0f04fa \
--hash=sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b \
--hash=sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e \
--hash=sha256:cae81479f77420d217def5f54b5b9d279804d17e982e0f2fa19b1d1e14ab5197 \
--hash=sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab \
--hash=sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79 \
--hash=sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2 \
--hash=sha256:d49e2314c373f4c2b39446fb1a45ed333c850e09d0c59ac79b72eb3b95397363 \
--hash=sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0 \
--hash=sha256:d64317d2587c70324b79861babb9c09f71fbb780bad212018874b2c013d8600e \
--hash=sha256:d77153e14b709fd8b8af6f66a3afbb9ed6e9fc5ccf0b6b7e1ced7b036a228782 \
--hash=sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925 \
--hash=sha256:dd333073e0cacdc3089525c7df7d39b211bcdf31fc2824e49d01c6b6187b07d0 \
--hash=sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b \
--hash=sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced \
--hash=sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c \
--hash=sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344 \
--hash=sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9 \
--hash=sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1
# via
# -r .github/scripts/requirements_dev.in
# pdf2image
# weasyprint
platformdirs==4.5.1 \
--hash=sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda \
--hash=sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31
platformdirs==4.5.0 \
--hash=sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312 \
--hash=sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3
# via virtualenv
pre-commit==4.5.1 \
--hash=sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77 \
--hash=sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61
pre-commit==4.3.0 \
--hash=sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8 \
--hash=sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16
# via -r .github/scripts/requirements_dev.in
pycparser==3.0 \
--hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \
--hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992
pycparser==2.23 \
--hash=sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2 \
--hash=sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934
# via cffi
pydyf==0.12.1 \
--hash=sha256:ea25b4e1fe7911195cb57067560daaa266639184e8335365cc3ee5214e7eaadc \
--hash=sha256:fbd7e759541ac725c29c506612003de393249b94310ea78ae44cb1d04b220095
pydyf==0.11.0 \
--hash=sha256:0aaf9e2ebbe786ec7a78ec3fbffa4cdcecde53fd6f563221d53c6bc1328848a3 \
--hash=sha256:394dddf619cca9d0c55715e3c55ea121a9bf9cbc780cdc1201a2427917b86b64
# via weasyprint
pyphen==0.17.2 \
--hash=sha256:3a07fb017cb2341e1d9ff31b8634efb1ae4dc4b130468c7c39dd3d32e7c3affd \
@@ -452,9 +442,9 @@ pyyaml==6.0.3 \
--hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \
--hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0
# via pre-commit
tinycss2==1.5.1 \
--hash=sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661 \
--hash=sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957
tinycss2==1.4.0 \
--hash=sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7 \
--hash=sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289
# via
# cssselect2
# weasyprint
@@ -462,17 +452,17 @@ tinyhtml5==2.0.0 \
--hash=sha256:086f998833da24c300c414d9fe81d9b368fd04cb9d2596a008421cbc705fcfcc \
--hash=sha256:13683277c5b176d070f82d099d977194b7a1e26815b016114f581a74bbfbf47e
# via weasyprint
unoserver==3.6 \
--hash=sha256:25c360fa194396a89cb79b4edd2735f8e4f0fd8531e59db3952114585bd7df05 \
--hash=sha256:e446bcb3638c51880f002aaeecab1cf74dfa9df81035f027f7ff2e081b6d7015
unoserver==3.4 \
--hash=sha256:3dcf2204013def1d1ddd3671f38b11346bdf349fef9728277462666a8a634419 \
--hash=sha256:64c24d33d4f65d680a2d9f676518cb28e7fd6c1f9d9a745c33e4a4cb59afdfcd
# via -r .github/scripts/requirements_dev.in
virtualenv==20.36.1 \
--hash=sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f \
--hash=sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba
virtualenv==20.35.4 \
--hash=sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c \
--hash=sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b
# via pre-commit
weasyprint==68.0 \
--hash=sha256:447f40898b747cb44ac31a5d493d512e7441fd56e13f63744c099383bbf9cda9 \
--hash=sha256:c2cb40c71b50837c5971f00171c9e4078e8c9912dd7c217f3e90e068f11e8aa1
weasyprint==66.0 \
--hash=sha256:82b0783b726fcd318e2c977dcdddca76515b30044bc7a830cc4fbe717582a6d0 \
--hash=sha256:da71dc87dc129ac9cffdc65e5477e90365ab9dbae45c744014ec1d06303dde40
# via -r .github/scripts/requirements_dev.in
webencodings==0.5.1 \
--hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \
@@ -481,27 +471,86 @@ webencodings==0.5.1 \
# cssselect2
# tinycss2
# tinyhtml5
zopfli==0.4.0 \
--hash=sha256:03181d48e719fcb6cf8340189c61e8f9883d8bbbdf76bf5212a74457f7d083c1 \
--hash=sha256:18b5f1570f64d4988482e4466f10ef5f2a30f687c19ad62a64560f2152dc89eb \
--hash=sha256:25e4863b8dc30e5d5309f87c106b0b7d3da4ed0e340b8a52b36d4471e797589f \
--hash=sha256:7d66337be6d5613dec55213e9ac28f378c41e2cc04fbad4a10748e4df774ca85 \
--hash=sha256:9097e8e1dfdb7f5aea5464e469946857e80502b6d29ba1b232450916bd4a74d1 \
--hash=sha256:a8ee992b2549e090cd3f0178bf606dd41a29e0613a04cdf5054224662c72dce6 \
--hash=sha256:b72a010d205d00b2855acc2302772067362f9ab5a012e3550662aec60d28e6b3 \
--hash=sha256:b8bdb41fbfdc4738b7bdc09ed7c1e951579fae192391a5e694d59bb186cdbec7 \
--hash=sha256:c3ba02a9a6ca90481d2b2f68bab038b310d63a1e3b5ae305e95a6599787ed941 \
--hash=sha256:d1b98ad47c434ef213444a03ef2f826eeec100144d64f6a57504b9893d3931ce \
--hash=sha256:f67d04280065e24cb9a4174cb6b3d1f763687f8cb2963aa135ad8f57c6995f5a \
--hash=sha256:f94e4dd7d76b4fe9f5d9229372be20d7f786164eea5152d1af1c34298c3d5975
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==26.0 \
--hash=sha256:3ce220a0a17915972fbf1ab451baae1521c4539e778b28127efa79b974aff0fa \
--hash=sha256:98436feffb9e31bc9339cf369fd55d3331b1580b6a6f1173bacacddcf9c34754
pip==25.3 \
--hash=sha256:8d0538dbbd7babbd207f261ed969c65de439f6bc9e5dbd3b3b9a77f25d95f343 \
--hash=sha256:9655943313a94722b7774661c21049070f6bbb0a1516bf02f7c8d5d9201514cd
# via -r .github/scripts/requirements_dev.in
setuptools==80.10.2 \
--hash=sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70 \
--hash=sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173
setuptools==80.9.0 \
--hash=sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 \
--hash=sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c
# via -r .github/scripts/requirements_dev.in
+18 -18
View File
@@ -12,25 +12,25 @@ distlib==0.4.0 \
--hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \
--hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d
# via virtualenv
filelock==3.20.3 \
--hash=sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1 \
--hash=sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1
filelock==3.20.0 \
--hash=sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2 \
--hash=sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4
# via virtualenv
identify==2.6.16 \
--hash=sha256:391ee4d77741d994189522896270b787aed8670389bfd60f326d677d64a6dfb0 \
--hash=sha256:846857203b5511bbe94d5a352a48ef2359532bc8f6727b5544077a0dcfb24980
identify==2.6.15 \
--hash=sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757 \
--hash=sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf
# via pre-commit
nodeenv==1.10.0 \
--hash=sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827 \
--hash=sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb
nodeenv==1.9.1 \
--hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \
--hash=sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9
# via pre-commit
platformdirs==4.5.1 \
--hash=sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda \
--hash=sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31
platformdirs==4.5.0 \
--hash=sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312 \
--hash=sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3
# via virtualenv
pre-commit==4.5.1 \
--hash=sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77 \
--hash=sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61
pre-commit==4.5.0 \
--hash=sha256:25e2ce09595174d9c97860a95609f9f852c0614ba602de3561e267547f2335e1 \
--hash=sha256:dc5a065e932b19fc1d4c653c6939068fe54325af8e741e74e88db4d28a4dd66b
# via -r .github/scripts/requirements_pre_commit.in
pyyaml==6.0.3 \
--hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \
@@ -107,7 +107,7 @@ pyyaml==6.0.3 \
--hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \
--hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0
# via pre-commit
virtualenv==20.36.1 \
--hash=sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f \
--hash=sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba
virtualenv==20.35.4 \
--hash=sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c \
--hash=sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b
# via pre-commit
+3 -3
View File
@@ -8,7 +8,7 @@ tomli-w==1.2.0 \
--hash=sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90 \
--hash=sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021
# via -r .github/scripts/requirements_sync_readme.in
tomlkit==0.14.0 \
--hash=sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680 \
--hash=sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064
tomlkit==0.13.3 \
--hash=sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1 \
--hash=sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0
# via -r .github/scripts/requirements_sync_readme.in
+5 -5
View File
@@ -130,7 +130,7 @@ jobs:
- name: Setup Node.js
if: matrix.variant.build_frontend == true
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@v4
with:
node-version: 22
cache: "npm"
@@ -185,14 +185,14 @@ jobs:
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libjavascriptcoregtk-4.0-dev libsoup2.4-dev libjavascriptcoregtk-4.1-dev libsoup-3.0-dev
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@v4
with:
node-version: 22
cache: "npm"
cache-dependency-path: frontend/package-lock.json
- name: Setup Rust
uses: dtolnay/rust-toolchain@4be9e76fd7c4901c61fb841f559994984270fce7 # stable
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
targets: ${{ (matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel') && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
@@ -291,7 +291,7 @@ jobs:
- name: Setup DigiCert KeyLocker
id: digicert-setup
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && (github.event_name == 'release' || github.ref == 'refs/heads/V2-master') }}
uses: digicert/ssm-code-signing@9476ceec3ea1c63298d4403b983e1ccf2556ff4c # v1.1.0
uses: digicert/ssm-code-signing@v1.1.0
env:
SM_API_KEY: ${{ secrets.SM_API_KEY }}
SM_CLIENT_CERT_FILE_B64: ${{ secrets.SM_CLIENT_CERT_FILE_B64 }}
@@ -399,7 +399,7 @@ jobs:
echo "Certificate imported successfully."
- name: Build Tauri app
uses: tauri-apps/tauri-action@51a9f1156b33df106d827c3a78f8f894946c5faa # v0.5.25
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
+4 -14
View File
@@ -33,11 +33,6 @@ jobs:
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- name: Determine build matrix
id: set-matrix
run: |
@@ -86,14 +81,14 @@ jobs:
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libjavascriptcoregtk-4.0-dev libsoup2.4-dev libjavascriptcoregtk-4.1-dev libsoup-3.0-dev
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@v4
with:
node-version: 22
cache: "npm"
cache-dependency-path: frontend/package-lock.json
- name: Setup Rust
uses: dtolnay/rust-toolchain@4be9e76fd7c4901c61fb841f559994984270fce7 # stable
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
targets: ${{ (matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel') && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
@@ -193,7 +188,7 @@ jobs:
- name: Setup DigiCert KeyLocker
id: digicert-setup
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }}
uses: digicert/ssm-code-signing@9476ceec3ea1c63298d4403b983e1ccf2556ff4c # v1.1.0
uses: digicert/ssm-code-signing@v1.1.0
env:
SM_API_KEY: ${{ secrets.SM_API_KEY }}
SM_CLIENT_CERT_FILE_B64: ${{ secrets.SM_CLIENT_CERT_FILE_B64 }}
@@ -312,7 +307,7 @@ jobs:
ls -la /usr/bin/hd* || echo "No hd* tools found"
- name: Build Tauri app
uses: tauri-apps/tauri-action@51a9f1156b33df106d827c3a78f8f894946c5faa # v0.5.25
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
@@ -644,11 +639,6 @@ jobs:
runs-on: ubuntu-latest
if: always()
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- name: Report build results
run: |
if [ "${{ needs.build.result }}" = "success" ]; then
-5
View File
@@ -128,11 +128,6 @@ jobs:
outputs:
frontend: ${{ steps.changes.outputs.frontend }}
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Check for file changes
@@ -303,23 +303,22 @@ public class EndpointConfiguration {
// Adding endpoints to "PageOps" group
addEndpointToGroup("PageOps", "remove-pages");
addEndpointToGroup("PageOps", "merge-pdfs");
addEndpointToGroup("PageOps", "split-pages");
addEndpointToGroup("PageOps", "rearrange-pages");
addEndpointToGroup("PageOps", "split-pdfs");
addEndpointToGroup("PageOps", "pdf-organizer");
addEndpointToGroup("PageOps", "rotate-pdf");
addEndpointToGroup("PageOps", "multi-page-layout");
addEndpointToGroup("PageOps", "booklet-imposition");
addEndpointToGroup("PageOps", "scale-pages");
addEndpointToGroup("PageOps", "crop");
addEndpointToGroup("PageOps", "extract-page");
addEndpointToGroup("PageOps", "pdf-to-single-page");
addEndpointToGroup("PageOps", "auto-split-pdf");
addEndpointToGroup("PageOps", "split-by-size-or-count");
addEndpointToGroup("PageOps", "overlay-pdf");
addEndpointToGroup("PageOps", "split-pdf-by-sections");
addEndpointToGroup("PageOps", "split-pdf-by-chapters");
addEndpointToGroup("PageOps", "add-page-numbers");
addEndpointToGroup("PageOps", "extract-pages");
// Adding endpoints to "Convert" group (Frontend has 15 convert endpoints)
// Adding endpoints to "Convert" group
addEndpointToGroup("Convert", "pdf-to-img");
addEndpointToGroup("Convert", "img-to-pdf");
addEndpointToGroup("Convert", "pdf-to-pdfa");
@@ -335,8 +334,6 @@ public class EndpointConfiguration {
addEndpointToGroup("Convert", "pdf-to-csv");
addEndpointToGroup("Convert", "pdf-to-markdown");
addEndpointToGroup("Convert", "eml-to-pdf");
addEndpointToGroup("Convert", "pdf-to-epub");
// Backend-only endpoints (not in frontend tool registry)
addEndpointToGroup("Convert", "pdf-to-vector");
addEndpointToGroup("Convert", "vector-to-pdf");
addEndpointToGroup("Convert", "pdf-to-video");
@@ -344,7 +341,6 @@ public class EndpointConfiguration {
addEndpointToGroup("Convert", "pdf-to-cbz");
addEndpointToGroup("Convert", "pdf-to-json");
addEndpointToGroup("Convert", "json-to-pdf");
addEndpointToGroup("Convert", "pdf-to-rtf");
// Adding endpoints to "Security" group
addEndpointToGroup("Security", "add-password");
@@ -355,49 +351,51 @@ public class EndpointConfiguration {
addEndpointToGroup("Security", "remove-cert-sign");
addEndpointToGroup("Security", "sanitize-pdf");
addEndpointToGroup("Security", "auto-redact");
addEndpointToGroup("Security", "validate-signature");
addEndpointToGroup("Security", "add-stamp");
addEndpointToGroup("Security", "unlock-pdf-forms");
// Backend-only endpoints (not in frontend tool registry endpoints)
addEndpointToGroup("Security", "redact");
addEndpointToGroup("Security", "validate-signature");
addEndpointToGroup("Security", "verify-pdf");
addEndpointToGroup("Security", "stamp");
addEndpointToGroup("Security", "sign");
// Adding endpoints to "Other" group
addEndpointToGroup("Other", "ocr-pdf");
addEndpointToGroup("Other", "add-image");
addEndpointToGroup("Other", "extract-images");
addEndpointToGroup("Other", "update-metadata");
addEndpointToGroup("Other", "change-metadata");
addEndpointToGroup("Other", "flatten");
addEndpointToGroup("Other", "unlock-pdf-forms");
addEndpointToGroup("Other", REMOVE_BLANKS);
addEndpointToGroup("Other", "remove-annotations");
addEndpointToGroup("Other", "compare");
addEndpointToGroup("Other", "add-page-numbers");
addEndpointToGroup("Other", "get-info-on-pdf");
addEndpointToGroup("Other", "remove-image-pdf");
addEndpointToGroup("Other", "add-attachments");
addEndpointToGroup("Other", "replace-invert-pdf");
addEndpointToGroup("Other", "edit-table-of-contents");
addEndpointToGroup("Other", "text-editor-pdf");
// Backend-only endpoints (not in frontend tool registry endpoints)
addEndpointToGroup("Other", "add-image");
addEndpointToGroup("Other", "compare");
addEndpointToGroup("Other", "view-pdf");
addEndpointToGroup("Other", "replace-and-invert-color-pdf");
addEndpointToGroup("Other", "multi-tool");
// Adding form-related endpoints to "Other" group
addEndpointToGroup("Other", "fields");
addEndpointToGroup("Other", "modify-fields");
addEndpointToGroup("Other", "delete-fields");
addEndpointToGroup("Other", "fill");
// Adding endpoints to "Advance" group
addEndpointToGroup("Advance", "adjust-contrast");
addEndpointToGroup("Advance", "compress-pdf");
addEndpointToGroup("Advance", "extract-image-scans");
addEndpointToGroup("Advance", "repair");
addEndpointToGroup("Advance", "auto-rename");
addEndpointToGroup("Advance", "handleData");
addEndpointToGroup("Advance", "scanner-effect");
addEndpointToGroup("Advance", "show-javascript");
addEndpointToGroup("Advance", "overlay-pdf");
// Backend-only endpoints
addEndpointToGroup("Advance", "adjust-contrast");
addEndpointToGroup("Advance", "pipeline");
addEndpointToGroup("Advance", "scanner-effect");
addEndpointToGroup("Advance", "auto-split-pdf");
addEndpointToGroup("Advance", "show-javascript");
addEndpointToGroup("Advance", "split-by-size-or-count");
addEndpointToGroup("Advance", "overlay-pdf");
addEndpointToGroup("Advance", "split-pdf-by-sections");
addEndpointToGroup("Advance", "edit-table-of-contents");
addEndpointToGroup("Advance", "split-pdf-by-chapters");
// CLI
addEndpointToGroup("CLI", "compress-pdf");
@@ -438,8 +436,8 @@ public class EndpointConfiguration {
// Java
addEndpointToGroup("Java", "merge-pdfs");
addEndpointToGroup("Java", "remove-pages");
addEndpointToGroup("Java", "split-pages");
addEndpointToGroup("Java", "rearrange-pages");
addEndpointToGroup("Java", "split-pdfs");
addEndpointToGroup("Java", "pdf-organizer");
addEndpointToGroup("Java", "rotate-pdf");
addEndpointToGroup("Java", "pdf-to-img");
addEndpointToGroup("Java", "img-to-pdf");
@@ -447,10 +445,9 @@ public class EndpointConfiguration {
addEndpointToGroup("Java", "remove-password");
addEndpointToGroup("Java", "change-permissions");
addEndpointToGroup("Java", "add-watermark");
addEndpointToGroup("Java", "add-stamp");
addEndpointToGroup("Java", "add-image");
addEndpointToGroup("Java", "extract-images");
addEndpointToGroup("Java", "update-metadata");
addEndpointToGroup("Java", "change-metadata");
addEndpointToGroup("Java", "cert-sign");
addEndpointToGroup("Java", "remove-cert-sign");
addEndpointToGroup("Java", "multi-page-layout");
@@ -462,6 +459,7 @@ public class EndpointConfiguration {
addEndpointToGroup("Java", "sanitize-pdf");
addEndpointToGroup("Java", "crop");
addEndpointToGroup("Java", "get-info-on-pdf");
addEndpointToGroup("Java", "extract-page");
addEndpointToGroup("Java", "pdf-to-single-page");
addEndpointToGroup("Java", "markdown-to-pdf");
addEndpointToGroup("Java", "show-javascript");
@@ -471,9 +469,7 @@ public class EndpointConfiguration {
addEndpointToGroup("Java", "split-by-size-or-count");
addEndpointToGroup("Java", "overlay-pdf");
addEndpointToGroup("Java", "split-pdf-by-sections");
addEndpointToGroup("Java", "split-pdf-by-chapters");
addEndpointToGroup("Java", REMOVE_BLANKS);
addEndpointToGroup("Java", "remove-annotations");
addEndpointToGroup("Java", "pdf-to-text");
addEndpointToGroup("Java", "remove-image-pdf");
addEndpointToGroup("Java", "pdf-to-markdown");
@@ -483,24 +479,15 @@ public class EndpointConfiguration {
addEndpointToGroup("Java", "pdf-to-cbz");
addEndpointToGroup("Java", "pdf-to-json");
addEndpointToGroup("Java", "json-to-pdf");
addEndpointToGroup("rar", "pdf-to-cbr");
addEndpointToGroup("Java", "pdf-to-video");
addEndpointToGroup("Java", "verify-pdf");
addEndpointToGroup("Java", "flatten");
addEndpointToGroup("Java", "unlock-pdf-forms");
addEndpointToGroup("Java", "validate-signature");
addEndpointToGroup("Java", "text-editor-pdf");
addEndpointToGroup("Java", "edit-table-of-contents");
addEndpointToGroup("Java", "pdf-to-epub");
addEndpointToGroup("Java", "eml-to-pdf");
addEndpointToGroup("Java", "handleData");
addEndpointToGroup("rar", "pdf-to-cbr");
// Javascript
addEndpointToGroup("Javascript", "rearrange-pages");
addEndpointToGroup("Javascript", "pdf-organizer");
addEndpointToGroup("Javascript", "sign");
addEndpointToGroup("Javascript", "compare");
addEndpointToGroup("Javascript", "adjust-contrast");
addEndpointToGroup("Javascript", "text-editor-pdf");
/* qpdf */
addEndpointToGroup("qpdf", "repair");
@@ -511,7 +498,6 @@ public class EndpointConfiguration {
addEndpointToGroup("Ghostscript", "compress-pdf");
addEndpointToGroup("Ghostscript", "crop");
addEndpointToGroup("Ghostscript", "replace-invert-pdf");
addEndpointToGroup("Ghostscript", "scanner-effect");
addEndpointToGroup("Ghostscript", "pdf-to-vector");
addEndpointToGroup("Ghostscript", "vector-to-pdf");
@@ -560,9 +546,6 @@ public class EndpointConfiguration {
// Pdftohtml dependent endpoints
addEndpointToGroup("Pdftohtml", "pdf-to-html");
addEndpointToGroup("Pdftohtml", "pdf-to-markdown");
// Calibre dependent endpoints
addEndpointToGroup("Calibre", "pdf-to-epub");
}
private void processEnvironmentConfigs() {
@@ -1,14 +1,10 @@
package stirling.software.common.configuration;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.annotation.Configuration;
@@ -45,7 +41,6 @@ public class RuntimePathConfig {
// Pipeline paths
private final String pipelineWatchedFoldersPath;
private final List<String> pipelineWatchedFoldersPaths;
private final String pipelineFinishedFoldersPath;
private final String pipelineDefaultWebUiConfigs;
private final String pipelinePath;
@@ -54,27 +49,20 @@ public class RuntimePathConfig {
this.properties = properties;
this.basePath = InstallationPathConfig.getPath();
this.pipelinePath = Path.of(basePath, "pipeline").toString();
String defaultWatchedFolders = Path.of(this.pipelinePath, "watchedFolders").toString();
String defaultFinishedFolders = Path.of(this.pipelinePath, "finishedFolders").toString();
String defaultWebUIConfigs = Path.of(this.pipelinePath, "defaultWebUIConfigs").toString();
System system = properties.getSystem();
CustomPaths customPaths = system.getCustomPaths();
Pipeline pipeline = customPaths.getPipeline();
this.pipelinePath =
this.pipelineWatchedFoldersPath =
resolvePath(
Path.of(basePath, "pipeline").toString(),
pipeline != null ? pipeline.getPipelineDir() : null);
String defaultWatchedFolders = Path.of(this.pipelinePath, "watchedFolders").toString();
String defaultFinishedFolders = Path.of(this.pipelinePath, "finishedFolders").toString();
String defaultWebUIConfigs = Path.of(this.pipelinePath, "defaultWebUIConfigs").toString();
List<String> watchedFoldersDirs =
sanitizePathList(pipeline != null ? pipeline.getWatchedFoldersDirs() : null);
this.pipelineWatchedFoldersPaths =
resolveWatchedFolderPaths(
defaultWatchedFolders,
watchedFoldersDirs,
pipeline != null ? pipeline.getWatchedFoldersDir() : null);
this.pipelineWatchedFoldersPath = this.pipelineWatchedFoldersPaths.get(0);
this.pipelineFinishedFoldersPath =
resolvePath(
defaultFinishedFolders,
@@ -84,9 +72,6 @@ public class RuntimePathConfig {
defaultWebUIConfigs,
pipeline != null ? pipeline.getWebUIConfigsDir() : null);
// Validate path conflicts after all paths are resolved
validatePipelinePaths();
boolean isDocker = isRunningInDocker();
// Initialize Operation paths
@@ -144,140 +129,6 @@ public class RuntimePathConfig {
return StringUtils.isNotBlank(customPath) ? customPath : defaultPath;
}
private List<String> resolveWatchedFolderPaths(
String defaultPath, List<String> watchedFoldersDirs, String legacyWatchedFolder) {
List<String> rawPaths = new ArrayList<>();
// Collect paths from new config
if (watchedFoldersDirs != null && !watchedFoldersDirs.isEmpty()) {
rawPaths.addAll(watchedFoldersDirs);
}
// Fall back to legacy config
else if (StringUtils.isNotBlank(legacyWatchedFolder)) {
rawPaths.add(legacyWatchedFolder);
}
// Fall back to default
else {
rawPaths.add(defaultPath);
}
// Validate, normalize, and deduplicate paths
List<String> validatedPaths = validateAndNormalizePaths(rawPaths);
// Ensure we have at least one valid path (critical for system to function)
if (validatedPaths.isEmpty()) {
log.warn(
"No valid watched folder paths configured, falling back to default: {}",
defaultPath);
validatedPaths.add(defaultPath);
}
// Detect overlapping paths (warning only, not blocking)
detectOverlappingPaths(validatedPaths);
return validatedPaths;
}
private List<String> sanitizePathList(List<String> paths) {
if (paths == null || paths.isEmpty()) {
return Collections.emptyList();
}
List<String> sanitized = new ArrayList<>();
for (String path : paths) {
if (StringUtils.isNotBlank(path)) {
sanitized.add(path.trim());
}
}
return sanitized;
}
private List<String> validateAndNormalizePaths(List<String> paths) {
Set<String> normalizedPaths = new LinkedHashSet<>(); // Preserves order, prevents duplicates
for (String pathStr : paths) {
if (StringUtils.isBlank(pathStr)) {
continue;
}
try {
// Normalize to absolute path
Path path = Paths.get(pathStr.trim()).toAbsolutePath().normalize();
String normalizedPath = path.toString();
// Check for duplicates
if (normalizedPaths.contains(normalizedPath)) {
log.debug("Skipping duplicate watched folder path: {}", pathStr);
continue;
}
normalizedPaths.add(normalizedPath);
log.info("Registered watched folder path: {}", normalizedPath);
} catch (InvalidPathException e) {
log.error(
"Invalid watched folder path '{}' - skipping: {}", pathStr, e.getMessage());
}
}
return new ArrayList<>(normalizedPaths);
}
private void detectOverlappingPaths(List<String> paths) {
for (int i = 0; i < paths.size(); i++) {
Path path1 = Paths.get(paths.get(i));
for (int j = i + 1; j < paths.size(); j++) {
Path path2 = Paths.get(paths.get(j));
// Check if one path is a parent of the other
if (path1.startsWith(path2)) {
log.warn(
"Watched folder path '{}' is nested inside '{}' - this may cause duplicate processing",
path1,
path2);
} else if (path2.startsWith(path1)) {
log.warn(
"Watched folder path '{}' is nested inside '{}' - this may cause duplicate processing",
path2,
path1);
}
}
}
}
private void validatePipelinePaths() {
try {
Path finishedPath = Paths.get(pipelineFinishedFoldersPath).toAbsolutePath().normalize();
for (String watchedPathStr : pipelineWatchedFoldersPaths) {
Path watchedPath = Paths.get(watchedPathStr).toAbsolutePath().normalize();
// Check if watched folder is same as finished folder
if (watchedPath.equals(finishedPath)) {
log.error(
"CRITICAL: Watched folder '{}' is the same as finished folder '{}' - this will cause processing loops!",
watchedPath,
finishedPath);
}
// Check if watched folder contains finished folder
else if (finishedPath.startsWith(watchedPath)) {
log.warn(
"Finished folder '{}' is nested inside watched folder '{}' - this may cause issues",
finishedPath,
watchedPath);
}
// Check if finished folder contains watched folder
else if (watchedPath.startsWith(finishedPath)) {
log.error(
"CRITICAL: Watched folder '{}' is nested inside finished folder '{}' - this will cause processing loops!",
watchedPath,
finishedPath);
}
}
} catch (Exception e) {
log.error("Error validating pipeline paths: {}", e.getMessage());
}
}
private boolean isRunningInDocker() {
return Files.exists(Path.of("/.dockerenv"));
}
@@ -459,9 +459,7 @@ public class ApplicationProperties {
@Data
public static class Pipeline {
private String pipelineDir;
private String watchedFoldersDir;
private List<String> watchedFoldersDirs = new ArrayList<>();
private String finishedFoldersDir;
private String webUIConfigsDir;
}
@@ -29,7 +29,7 @@ public class FileMonitor {
private final ConcurrentHashMap.KeySetView<Path, Boolean> readyForProcessingFiles;
private final WatchService watchService;
private final Predicate<Path> pathFilter;
private final List<Path> rootDirs;
private final Path rootDir;
private Set<Path> stagingFiles;
/**
@@ -47,28 +47,8 @@ public class FileMonitor {
this.pathFilter = pathFilter;
this.readyForProcessingFiles = ConcurrentHashMap.newKeySet();
this.watchService = FileSystems.getDefault().newWatchService();
List<String> watchedFoldersDirs = runtimePathConfig.getPipelineWatchedFoldersPaths();
List<Path> validRootDirs = new ArrayList<>();
for (String pathStr : watchedFoldersDirs) {
try {
Path path = Path.of(pathStr);
validRootDirs.add(path);
log.info("Monitoring directory: {}", path);
} catch (Exception e) {
log.error(
"Failed to initialize monitoring for path '{}': {}",
pathStr,
e.getMessage());
}
}
this.rootDirs = Collections.unmodifiableList(validRootDirs);
if (this.rootDirs.isEmpty()) {
log.error("No valid directories to monitor - FileMonitor will not function");
}
log.info("Monitoring directory: {}", runtimePathConfig.getPipelineWatchedFoldersPath());
this.rootDir = Path.of(runtimePathConfig.getPipelineWatchedFoldersPath());
}
private boolean shouldNotProcess(Path path) {
@@ -105,15 +85,13 @@ public class FileMonitor {
readyForProcessingFiles.clear();
if (path2KeyMapping.isEmpty()) {
log.warn("Not monitoring any directories; attempting to re-register root paths.");
for (Path rootDir : rootDirs) {
if (Files.exists(
rootDir)) { // if the root directory exists, re-register the root directory
try {
recursivelyRegisterEntry(rootDir);
} catch (IOException e) {
log.error("unable to register monitoring for {}", rootDir, e);
}
log.warn("not monitoring any directory, even the root directory itself: {}", rootDir);
if (Files.exists(
rootDir)) { // if the root directory exists, re-register the root directory
try {
recursivelyRegisterEntry(rootDir);
} catch (IOException e) {
log.error("unable to register monitoring", e);
}
}
}
@@ -9,7 +9,6 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.FileTime;
import java.time.Instant;
import java.util.List;
import java.util.function.Predicate;
import org.junit.jupiter.api.BeforeEach;
@@ -35,8 +34,7 @@ class FileMonitorTest {
@BeforeEach
void setUp() throws IOException {
when(runtimePathConfig.getPipelineWatchedFoldersPaths())
.thenReturn(List.of(tempDir.toString()));
when(runtimePathConfig.getPipelineWatchedFoldersPath()).thenReturn(tempDir.toString());
// This mock is used in all tests except testPathFilter
// We use lenient to avoid UnnecessaryStubbingException in that test
@@ -118,8 +118,7 @@ public class ExternalAppDepConfig {
for (String group : affectedGroups) {
List<String> affectedFeatures = getAffectedFeatures(group);
endpointConfiguration.disableGroup(
group, EndpointConfiguration.DisableReason.DEPENDENCY);
endpointConfiguration.disableGroup(group);
log.warn(
"Missing dependency: {} - Disabling group: {} (Affected features: {})",
command,
@@ -144,8 +143,7 @@ public class ExternalAppDepConfig {
commandToGroupMapping.getOrDefault(
command, List.of("Weasyprint"));
for (String group : affectedGroups) {
endpointConfiguration.disableGroup(
group, EndpointConfiguration.DisableReason.DEPENDENCY);
endpointConfiguration.disableGroup(group);
}
log.warn(
"WeasyPrint version {} is below required {} - disabling"
@@ -174,8 +172,7 @@ public class ExternalAppDepConfig {
List<String> affectedGroups =
commandToGroupMapping.getOrDefault(command, List.of("qpdf"));
for (String group : affectedGroups) {
endpointConfiguration.disableGroup(
group, EndpointConfiguration.DisableReason.DEPENDENCY);
endpointConfiguration.disableGroup(group);
}
log.warn(
"qpdf version {} is below required {} - disabling group(s): {}",
@@ -229,8 +226,7 @@ public class ExternalAppDepConfig {
int ec = runAndWait(List.of(python, "-c", "import cv2"), DEFAULT_TIMEOUT).exitCode();
if (ec != 0) {
List<String> openCVFeatures = getAffectedFeatures("OpenCV");
endpointConfiguration.disableGroup(
"OpenCV", EndpointConfiguration.DisableReason.DEPENDENCY);
endpointConfiguration.disableGroup("OpenCV");
log.warn(
"OpenCV not available in Python - Disabling OpenCV features: {}",
String.join(", ", openCVFeatures));
@@ -240,10 +236,8 @@ public class ExternalAppDepConfig {
private void disablePythonAndOpenCV(String reason) {
List<String> pythonFeatures = getAffectedFeatures("Python");
List<String> openCVFeatures = getAffectedFeatures("OpenCV");
endpointConfiguration.disableGroup(
"Python", EndpointConfiguration.DisableReason.DEPENDENCY);
endpointConfiguration.disableGroup(
"OpenCV", EndpointConfiguration.DisableReason.DEPENDENCY);
endpointConfiguration.disableGroup("Python");
endpointConfiguration.disableGroup("OpenCV");
log.warn(
"Missing dependency: Python (reason: {}) - Disabling Python features: {} and OpenCV"
+ " features: {}",
@@ -106,29 +106,16 @@ public class WebMvcConfig implements WebMvcConfigurer {
.allowCredentials(true)
.maxAge(3600);
} else if (hasConfiguredOrigins) {
// Use user-configured origins + always include Tauri origins for desktop app support
// Use user-configured origins
logger.info(
"Configuring CORS with allowed origins: {}",
applicationProperties.getSystem().getCorsAllowedOrigins());
// Combine user-configured origins with Tauri origins
java.util.List<String> allOrigins =
new java.util.ArrayList<>(
applicationProperties.getSystem().getCorsAllowedOrigins());
// Always include Tauri origins for desktop app compatibility
// Tauri v1 uses tauri://localhost, v2 uses http(s)://tauri.localhost
if (!allOrigins.contains("tauri://localhost")) {
allOrigins.add("tauri://localhost");
}
if (!allOrigins.contains("http://tauri.localhost")) {
allOrigins.add("http://tauri.localhost");
}
if (!allOrigins.contains("https://tauri.localhost")) {
allOrigins.add("https://tauri.localhost");
}
String[] allowedOrigins = allOrigins.toArray(new String[0]);
String[] allowedOrigins =
applicationProperties
.getSystem()
.getCorsAllowedOrigins()
.toArray(new String[0]);
registry.addMapping("/**")
.allowedOriginPatterns(allowedOrigins)
@@ -7,13 +7,11 @@ import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.time.LocalDateTime;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.imageio.ImageIO;
@@ -60,13 +58,6 @@ public class StampController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final TempFileManager tempFileManager;
private static final int MAX_DATE_FORMAT_LENGTH = 50;
private static final Pattern SAFE_DATE_FORMAT_PATTERN =
Pattern.compile("^[yMdHhmsS/\\-:\\s.,'+EGuwWDFzZXa]+$");
private static final Pattern CUSTOM_DATE_PATTERN = Pattern.compile("@date\\{([^}]{1,50})\\}");
// Placeholder for escaped @ symbol (using Unicode private use area)
private static final String ESCAPED_AT_PLACEHOLDER = "\uE000ESCAPED_AT\uE000";
/**
* 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
@@ -175,9 +166,7 @@ public class StampController {
overrideX,
overrideY,
margin,
customColor,
pageIndex,
pdfFileName);
customColor);
} else if ("image".equalsIgnoreCase(stampType)) {
addImageStamp(
contentStream,
@@ -212,11 +201,9 @@ public class StampController {
float fontSize,
String alphabet,
float overrideX, // X override
float overrideY, // Y override
float overrideY,
float margin,
String colorString,
int currentPageNumber,
String filename)
String colorString) // Y override
throws IOException {
String resourceDir;
PDFont font = new PDType1Font(Standard14Fonts.FontName.HELVETICA);
@@ -244,6 +231,8 @@ public class StampController {
}
}
contentStream.setFont(font, fontSize);
Color redactColor;
try {
if (!colorString.startsWith("#")) {
@@ -251,54 +240,47 @@ public class StampController {
}
redactColor = Color.decode(colorString);
} catch (NumberFormatException e) {
redactColor = Color.LIGHT_GRAY;
}
contentStream.setNonStrokingColor(redactColor);
PDRectangle pageSize = page.getMediaBox();
float x, y;
if (overrideX >= 0 && overrideY >= 0) {
// Use override values if provided
x = overrideX;
y = overrideY;
} else {
x = calculatePositionX(pageSize, position, fontSize, font, fontSize, stampText, margin);
y =
calculatePositionY(
pageSize, position, calculateTextCapHeight(font, fontSize), margin);
}
String currentDate = LocalDate.now().toString();
String currentTime = LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss"));
int pageCount = document.getNumberOfPages();
String processedStampText =
processStampText(stampText, currentPageNumber, pageCount, filename, document);
stampText
.replace("@date", currentDate)
.replace("@time", currentTime)
.replace("@page_count", String.valueOf(pageCount));
String normalizedText =
// Split the stampText into multiple lines
String[] lines =
RegexPatternUtils.getInstance()
.getEscapedNewlinePattern()
.matcher(processedStampText)
.replaceAll("\n");
String[] lines = normalizedText.split("\\r?\\n");
PDRectangle pageSize = page.getMediaBox();
// Use fontSize directly (default 40 if not specified)
float effectiveFontSize = fontSize > 0 ? fontSize : 40f;
contentStream.setFont(font, effectiveFontSize);
.split(processedStampText);
// Calculate dynamic line height based on font ascent and descent
float ascent = font.getFontDescriptor().getAscent();
float descent = font.getFontDescriptor().getDescent();
float lineHeight = ((ascent - descent) / 1000) * effectiveFontSize;
float maxLineWidth = 0;
for (String line : lines) {
float lineWidth = calculateTextWidth(line, font, effectiveFontSize);
if (lineWidth > maxLineWidth) {
maxLineWidth = lineWidth;
}
}
float totalTextHeight = lines.length * lineHeight;
float x, y;
if (overrideX >= 0 && overrideY >= 0) {
x = overrideX;
y = overrideY;
} else {
x = calculatePositionX(pageSize, position, maxLineWidth, margin);
y = calculatePositionY(pageSize, position, totalTextHeight, margin);
}
float lineHeight = ((ascent - descent) / 1000) * fontSize;
contentStream.beginText();
for (int i = 0; i < lines.length; i++) {
@@ -311,140 +293,6 @@ public class StampController {
contentStream.endText();
}
/**
* Process stamp text by replacing all @commands with their actual values. Supported commands:
*
* <p>Date & Time:
*
* <ul>
* <li>@date - Current date (YYYY-MM-DD)
* <li>@time - Current time (HH:mm:ss)
* <li>@datetime - Current date and time (YYYY-MM-DD HH:mm:ss)
* <li>@date{format} - Custom date/time format (e.g., @date{dd/MM/yyyy})
* <li>@year - Current year (4 digits)
* <li>@month - Current month (01-12)
* <li>@day - Current day of month (01-31)
* </ul>
*
* <p>Page Information:
*
* <ul>
* <li>@page_number or @page - Current page number
* <li>@total_pages or @page_count - Total number of pages
* </ul>
*
* <p>File Information:
*
* <ul>
* <li>@filename - Original filename (without extension)
* <li>@filename_full - Original filename (with extension)
* </ul>
*
* <p>Document Metadata:
*
* <ul>
* <li>@author - Document author (from PDF metadata)
* <li>@title - Document title (from PDF metadata)
* <li>@subject - Document subject (from PDF metadata)
* </ul>
*
* <p>Other:
*
* <ul>
* <li>@uuid - Short unique identifier (8 characters)
* </ul>
*/
private String processStampText(
String stampText,
int currentPageNumber,
int totalPages,
String filename,
PDDocument document) {
if (stampText == null || stampText.isEmpty()) {
return "";
}
// Handle escaped @@ sequences first - replace with placeholder to preserve literal @
String result = stampText.replace("@@", ESCAPED_AT_PLACEHOLDER);
LocalDateTime now = LocalDateTime.now();
String currentDate = now.toLocalDate().toString();
String currentTime = now.toLocalTime().format(DateTimeFormatter.ofPattern("HH:mm:ss"));
String currentDateTime = now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
String filenameWithoutExt = filename != null ? filename : "";
if (filename != null && filename.contains(".")) {
int lastDot = filename.lastIndexOf('.');
if (lastDot > 0) { // Ensure there's actually a name before the dot
filenameWithoutExt = filename.substring(0, lastDot);
}
}
String author = "";
String title = "";
String subject = "";
if (document != null && document.getDocumentInformation() != null) {
var info = document.getDocumentInformation();
author = info.getAuthor() != null ? info.getAuthor() : "";
title = info.getTitle() != null ? info.getTitle() : "";
subject = info.getSubject() != null ? info.getSubject() : "";
}
String uuid = UUID.randomUUID().toString().substring(0, 8);
// Process @date{format} with custom format first (must be before simple @date)
Matcher matcher = CUSTOM_DATE_PATTERN.matcher(result);
StringBuilder sb = new StringBuilder();
while (matcher.find()) {
String format = matcher.group(1);
String replacement = processCustomDateFormat(format, now);
matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement));
}
matcher.appendTail(sb);
result = sb.toString();
result =
result.replace("@datetime", currentDateTime)
.replace("@date", currentDate)
.replace("@time", currentTime)
.replace("@year", String.valueOf(now.getYear()))
.replace("@month", String.format("%02d", now.getMonthValue()))
.replace("@day", String.format("%02d", now.getDayOfMonth()))
.replace("@page_number", String.valueOf(currentPageNumber))
.replace(
"@page_count", String.valueOf(totalPages)) // Must come before @page
.replace("@total_pages", String.valueOf(totalPages))
.replace(
"@page",
String.valueOf(currentPageNumber)) // Must come after @page_count
.replace("@filename_full", filename != null ? filename : "")
.replace("@filename", filenameWithoutExt)
.replace("@author", author)
.replace("@title", title)
.replace("@subject", subject)
.replace("@uuid", uuid);
result = result.replace(ESCAPED_AT_PLACEHOLDER, "@");
return result;
}
private String processCustomDateFormat(String format, LocalDateTime now) {
if (format == null || format.length() > MAX_DATE_FORMAT_LENGTH) {
return "[invalid format: too long]";
}
if (!SAFE_DATE_FORMAT_PATTERN.matcher(format).matches()) {
return "[invalid format]";
}
try {
return now.format(DateTimeFormatter.ofPattern(format));
} catch (IllegalArgumentException e) {
return "[invalid format: " + format + "]";
}
}
private void addImageStamp(
PDPageContentStream contentStream,
MultipartFile stampImage,
@@ -481,8 +329,8 @@ public class StampController {
x = overrideX;
y = overrideY;
} else {
x = calculatePositionX(pageSize, position, desiredPhysicalWidth, margin);
y = calculatePositionY(pageSize, position, desiredPhysicalHeight, margin);
x = calculatePositionX(pageSize, position, desiredPhysicalWidth, null, 0, null, margin);
y = calculatePositionY(pageSize, position, fontSize, margin);
}
contentStream.saveGraphicsState();
@@ -493,14 +341,23 @@ public class StampController {
}
private float calculatePositionX(
PDRectangle pageSize, int position, float contentWidth, float margin) {
PDRectangle pageSize,
int position,
float contentWidth,
PDFont font,
float fontSize,
String text,
float margin)
throws IOException {
float actualWidth =
(text != null) ? calculateTextWidth(text, font, fontSize) : contentWidth;
return switch (position % 3) {
case 1: // Left
yield pageSize.getLowerLeftX() + margin;
case 2: // Center
yield (pageSize.getWidth() - contentWidth) / 2;
yield (pageSize.getWidth() - actualWidth) / 2;
case 0: // Right
yield pageSize.getUpperRightX() - contentWidth - margin;
yield pageSize.getUpperRightX() - actualWidth - margin;
default:
yield 0;
};
@@ -509,12 +366,12 @@ public class StampController {
private float calculatePositionY(
PDRectangle pageSize, int position, float height, float margin) {
return switch ((position - 1) / 3) {
case 0: // Top - first line near the top
yield pageSize.getUpperRightY() - margin;
case 1: // Middle - center of text block at page center
yield (pageSize.getHeight() + height) / 2;
case 2: // Bottom - first line positioned so last line is at bottom margin
yield pageSize.getLowerLeftY() + margin + height;
case 0: // Top
yield pageSize.getUpperRightY() - height - margin;
case 1: // Middle
yield (pageSize.getHeight() - height) / 2;
case 2: // Bottom
yield pageSize.getLowerLeftY() + margin;
default:
yield 0;
};
@@ -523,4 +380,8 @@ public class StampController {
private float calculateTextWidth(String text, PDFont font, float fontSize) throws IOException {
return font.getStringWidth(text) / 1000 * fontSize;
}
private float calculateTextCapHeight(PDFont font, float fontSize) {
return font.getFontDescriptor().getCapHeight() / 1000 * fontSize;
}
}
@@ -3,7 +3,6 @@ package stirling.software.SPDF.controller.api.pipeline;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystemException;
import java.nio.file.FileVisitOption;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -15,7 +14,6 @@ import java.time.LocalDate;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
@@ -43,20 +41,14 @@ import stirling.software.common.util.FileMonitor;
@Slf4j
public class PipelineDirectoryProcessor {
private static final int MAX_DIRECTORY_DEPTH = 50; // Prevent excessive recursion
private final ObjectMapper objectMapper;
private final ApiDocService apiDocService;
private final PipelineProcessor processor;
private final FileMonitor fileMonitor;
private final PostHogService postHogService;
private final List<String> watchedFoldersDirs;
private final String watchedFoldersDir;
private final String finishedFoldersDir;
// Track processed directories in current scan to prevent duplicates
private final ThreadLocal<java.util.Set<Path>> processedDirsInScan =
ThreadLocal.withInitial(java.util.HashSet::new);
public PipelineDirectoryProcessor(
ObjectMapper objectMapper,
ApiDocService apiDocService,
@@ -69,26 +61,13 @@ public class PipelineDirectoryProcessor {
this.processor = processor;
this.fileMonitor = fileMonitor;
this.postHogService = postHogService;
this.watchedFoldersDirs = runtimePathConfig.getPipelineWatchedFoldersPaths();
this.watchedFoldersDir = runtimePathConfig.getPipelineWatchedFoldersPath();
this.finishedFoldersDir = runtimePathConfig.getPipelineFinishedFoldersPath();
}
@Scheduled(fixedRate = 60000)
public void scanFolders() {
// Clear the processed directories set for this scan cycle
processedDirsInScan.get().clear();
try {
for (String watchedFoldersDir : watchedFoldersDirs) {
scanWatchedFolder(Paths.get(watchedFoldersDir).toAbsolutePath());
}
} finally {
// Clean up ThreadLocal to prevent memory leaks
processedDirsInScan.remove();
}
}
private void scanWatchedFolder(Path watchedFolderPath) {
Path watchedFolderPath = Paths.get(watchedFoldersDir).toAbsolutePath();
if (!Files.exists(watchedFolderPath)) {
try {
Files.createDirectories(watchedFolderPath);
@@ -99,34 +78,16 @@ public class PipelineDirectoryProcessor {
}
}
// Validate the path is a directory and readable
if (!Files.isDirectory(watchedFolderPath)) {
log.error("Path is not a directory: {}", watchedFolderPath);
return;
}
if (!Files.isReadable(watchedFolderPath)) {
log.error("Directory is not readable: {}", watchedFolderPath);
return;
}
try {
// Use FOLLOW_LINKS to follow symlinks, with max depth to prevent infinite loops
Files.walkFileTree(
watchedFolderPath,
EnumSet.of(FileVisitOption.FOLLOW_LINKS),
MAX_DIRECTORY_DEPTH,
new SimpleFileVisitor<>() {
@Override
public FileVisitResult preVisitDirectory(
Path dir, BasicFileAttributes attrs) {
try {
String dirName =
dir.getFileName() != null
? dir.getFileName().toString()
: "";
// Skip root directory and "processing" subdirectories
if (!dir.equals(watchedFolderPath)
&& !"processing".equals(dirName)) {
if (!dir.equals(watchedFolderPath) && !dir.endsWith("processing")) {
handleDirectory(dir);
}
} catch (Exception e) {
@@ -137,11 +98,8 @@ public class PipelineDirectoryProcessor {
@Override
public FileVisitResult visitFileFailed(Path path, IOException exc) {
// Handle broken symlinks, permission issues, or inaccessible
// directories
if (exc != null) {
log.debug("Cannot access path '{}': {}", path, exc.getMessage());
}
// Handle broken symlinks or inaccessible directories
log.error("Error accessing path: {}", path, exc);
return FileVisitResult.CONTINUE;
}
});
@@ -151,17 +109,6 @@ public class PipelineDirectoryProcessor {
}
public void handleDirectory(Path dir) throws IOException {
// Normalize path to absolute to prevent duplicate processing from different path
// representations
Path normalizedDir = dir.toAbsolutePath().normalize();
// Check if we've already processed this directory in this scan cycle
java.util.Set<Path> processedDirs = processedDirsInScan.get();
if (!processedDirs.add(normalizedDir)) {
log.debug("Directory already processed in this scan cycle: {}", normalizedDir);
return;
}
log.info("Handling directory: {}", dir);
Path processingDir = createProcessingDirectory(dir);
Optional<Path> jsonFileOptional = findJsonFile(dir);
@@ -32,8 +32,8 @@ public class AddStampRequest extends PDFWithPageNums {
private String alphabet = "roman";
@Schema(
description = "The font size of the stamp text and image in points.",
defaultValue = "40",
description = "The font size of the stamp text and image",
defaultValue = "30",
requiredMode = Schema.RequiredMode.REQUIRED)
private float fontSize;
@@ -203,9 +203,7 @@ system:
name: postgres # set the name of your database. Should match the name of the database you create
customPaths:
pipeline:
pipelineDir: "" # Defaults to /pipeline
watchedFoldersDir: "" # Defaults to /pipeline/watchedFolders
watchedFoldersDirs: [] # List of watched folder directories. Defaults to watchedFoldersDir or /pipeline/watchedFolders.
finishedFoldersDir: "" # Defaults to /pipeline/finishedFolders
operations:
weasyprint: "" # Defaults to /opt/venv/bin/weasyprint
@@ -1,567 +0,0 @@
package stirling.software.SPDF.controller.api.misc;
import static org.junit.jupiter.api.Assertions.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDDocumentInformation;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.TempFileManager;
@ExtendWith(MockitoExtension.class)
class StampControllerTest {
@Mock private CustomPDFDocumentFactory pdfDocumentFactory;
@Mock private TempFileManager tempFileManager;
@InjectMocks private StampController stampController;
private Method processStampTextMethod;
private Method processCustomDateFormatMethod;
@BeforeEach
void setUp() throws NoSuchMethodException {
processStampTextMethod =
StampController.class.getDeclaredMethod(
"processStampText",
String.class,
int.class,
int.class,
String.class,
PDDocument.class);
processStampTextMethod.setAccessible(true);
processCustomDateFormatMethod =
StampController.class.getDeclaredMethod(
"processCustomDateFormat", String.class, LocalDateTime.class);
processCustomDateFormatMethod.setAccessible(true);
}
private String invokeProcessStampText(
String stampText, int pageNumber, int totalPages, String filename, PDDocument document)
throws Exception {
try {
return (String)
processStampTextMethod.invoke(
stampController, stampText, pageNumber, totalPages, filename, document);
} catch (InvocationTargetException e) {
throw (Exception) e.getCause();
}
}
private String invokeProcessCustomDateFormat(String format, LocalDateTime now)
throws Exception {
try {
return (String) processCustomDateFormatMethod.invoke(stampController, format, now);
} catch (InvocationTargetException e) {
throw (Exception) e.getCause();
}
}
@Nested
@DisplayName("Basic Variable Substitution Tests")
class BasicVariableTests {
@Test
@DisplayName("Should replace @page_number with current page")
void testPageNumberReplacement() throws Exception {
String result = invokeProcessStampText("Page @page_number", 5, 20, "test.pdf", null);
assertEquals("Page 5", result);
}
@Test
@DisplayName("Should replace @total_pages with total page count")
void testTotalPagesReplacement() throws Exception {
String result =
invokeProcessStampText("of @total_pages pages", 1, 100, "test.pdf", null);
assertEquals("of 100 pages", result);
}
@Test
@DisplayName("Should replace combined page variables")
void testCombinedPageVariables() throws Exception {
String result =
invokeProcessStampText(
"Page @page_number of @total_pages", 5, 20, "test.pdf", null);
assertEquals("Page 5 of 20", result);
}
@Test
@DisplayName("Should replace @page alias")
void testPageAlias() throws Exception {
String result = invokeProcessStampText("Page @page", 7, 10, "test.pdf", null);
assertEquals("Page 7", result);
}
@Test
@DisplayName("Should replace @page_count alias for total pages")
void testPageCountAlias() throws Exception {
String result = invokeProcessStampText("Total: @page_count", 1, 50, "test.pdf", null);
assertEquals("Total: 50", result);
}
}
@Nested
@DisplayName("Filename Variable Tests")
class FilenameTests {
@Test
@DisplayName("Should replace @filename with filename without extension")
void testFilenameWithoutExtension() throws Exception {
String result = invokeProcessStampText("File: @filename", 1, 1, "document.pdf", null);
assertEquals("File: document", result);
}
@Test
@DisplayName("Should replace @filename_full with full filename")
void testFilenameWithExtension() throws Exception {
String result =
invokeProcessStampText("File: @filename_full", 1, 1, "document.pdf", null);
assertEquals("File: document.pdf", result);
}
@Test
@DisplayName("Should handle filename without extension")
void testFilenameWithoutDot() throws Exception {
String result = invokeProcessStampText("@filename", 1, 1, "document", null);
assertEquals("document", result);
}
@Test
@DisplayName("Should handle null filename")
void testNullFilename() throws Exception {
String result = invokeProcessStampText("File: @filename", 1, 1, null, null);
assertEquals("File: ", result);
}
@Test
@DisplayName("Should handle filename with multiple dots")
void testFilenameMultipleDots() throws Exception {
String result = invokeProcessStampText("@filename", 1, 1, "my.document.v2.pdf", null);
assertEquals("my.document.v2", result);
}
@Test
@DisplayName("Should handle hidden file (starts with dot)")
void testHiddenFile() throws Exception {
String result = invokeProcessStampText("@filename", 1, 1, ".hidden.pdf", null);
assertEquals(".hidden", result);
}
}
@Nested
@DisplayName("Date/Time Variable Tests")
class DateTimeTests {
@Test
@DisplayName("Should replace @date with current date")
void testDateReplacement() throws Exception {
String result = invokeProcessStampText("Date: @date", 1, 1, "test.pdf", null);
assertTrue(
result.matches("Date: \\d{4}-\\d{2}-\\d{2}"),
"Date should match YYYY-MM-DD format");
}
@Test
@DisplayName("Should replace @time with current time")
void testTimeReplacement() throws Exception {
String result = invokeProcessStampText("Time: @time", 1, 1, "test.pdf", null);
assertTrue(
result.matches("Time: \\d{2}:\\d{2}:\\d{2}"),
"Time should match HH:mm:ss format");
}
@Test
@DisplayName("Should replace @datetime with combined date and time")
void testDateTimeReplacement() throws Exception {
String result = invokeProcessStampText("@datetime", 1, 1, "test.pdf", null);
// DateTime format: YYYY-MM-DD HH:mm:ss
assertTrue(
result.matches("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}"),
"DateTime should match YYYY-MM-DD HH:mm:ss format");
}
@Test
@DisplayName("Should replace @year with current year")
void testYearReplacement() throws Exception {
String result = invokeProcessStampText("© @year", 1, 1, "test.pdf", null);
int currentYear = LocalDateTime.now().getYear();
assertEquals("© " + currentYear, result);
}
@Test
@DisplayName("Should replace @month with zero-padded month")
void testMonthReplacement() throws Exception {
String result = invokeProcessStampText("Month: @month", 1, 1, "test.pdf", null);
assertTrue(result.matches("Month: \\d{2}"), "Month should be zero-padded");
}
@Test
@DisplayName("Should replace @day with zero-padded day")
void testDayReplacement() throws Exception {
String result = invokeProcessStampText("Day: @day", 1, 1, "test.pdf", null);
assertTrue(result.matches("Day: \\d{2}"), "Day should be zero-padded");
}
}
@Nested
@DisplayName("Custom Date Format Tests")
class CustomDateFormatTests {
@Test
@DisplayName("Should handle custom date format dd/MM/yyyy")
void testCustomDateFormatSlash() throws Exception {
String result = invokeProcessStampText("@date{dd/MM/yyyy}", 1, 1, "test.pdf", null);
assertTrue(
result.matches("\\d{2}/\\d{2}/\\d{4}"),
"Should match dd/MM/yyyy format: " + result);
}
@Test
@DisplayName("Should handle custom date format with time")
void testCustomDateFormatWithTime() throws Exception {
String result =
invokeProcessStampText("@date{yyyy-MM-dd HH:mm}", 1, 1, "test.pdf", null);
assertTrue(
result.matches("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}"),
"Should match yyyy-MM-dd HH:mm format: " + result);
}
@Test
@DisplayName("Should handle multiple custom date formats in same text")
void testMultipleCustomDateFormats() throws Exception {
String result =
invokeProcessStampText(
"Start: @date{dd/MM/yyyy} End: @date{yyyy}", 1, 1, "test.pdf", null);
assertTrue(result.contains("/"), "Should contain slash from first format");
// Should have year twice (once with slashes, once alone)
}
}
@Nested
@DisplayName("Custom Date Format Security Tests")
class CustomDateFormatSecurityTests {
@Test
@DisplayName("Should not match format that is too long - regex won't capture it")
void testFormatTooLong() throws Exception {
String longFormat = "y".repeat(51); // 51 chars, over the 50 char regex limit
String result =
invokeProcessStampText("@date{" + longFormat + "}", 1, 1, "test.pdf", null);
// The CUSTOM_DATE_PATTERN only captures up to 50 chars, so this won't match
// The @date part will be replaced by simple replacement, leaving {yyy...}
assertTrue(
result.contains("{"), "Should contain { because regex didn't match: " + result);
}
@Test
@DisplayName("Should reject format with unsafe characters - shell injection attempt")
void testShellInjectionAttempt() throws Exception {
String result =
invokeProcessStampText("@date{yyyy-MM-dd$(rm -rf /)}", 1, 1, "test.pdf", null);
assertEquals("[invalid format]", result);
}
@Test
@DisplayName("Should reject format with unsafe characters - semicolon")
void testSemicolonInjection() throws Exception {
String result = invokeProcessStampText("@date{yyyy;rm}", 1, 1, "test.pdf", null);
assertEquals("[invalid format]", result);
}
@Test
@DisplayName("Should reject format with unsafe characters - backticks")
void testBacktickInjection() throws Exception {
String result = invokeProcessStampText("@date{`whoami`}", 1, 1, "test.pdf", null);
assertEquals("[invalid format]", result);
}
@ParameterizedTest
@ValueSource(strings = {"$(cmd)", "`cmd`", ";cmd", "|cmd", "&cmd", "<cmd", ">cmd"})
@DisplayName("Should reject various injection attempts")
void testVariousInjectionAttempts(String injection) throws Exception {
String result =
invokeProcessStampText("@date{yyyy" + injection + "}", 1, 1, "test.pdf", null);
assertEquals("[invalid format]", result);
}
@Test
@DisplayName("Should accept valid format characters")
void testValidFormatCharacters() throws Exception {
// All these should be valid based on SAFE_DATE_FORMAT_PATTERN: yMdHhmsS/-:.,
// '+EGuwWDFzZXa and space
String result =
invokeProcessStampText("@date{yyyy-MM-dd HH:mm:ss}", 1, 1, "test.pdf", null);
assertFalse(
result.startsWith("[invalid"), "Valid format should be accepted: " + result);
}
@Test
@DisplayName("Should handle invalid DateTimeFormatter pattern gracefully")
void testInvalidFormatterPattern() throws Exception {
LocalDateTime now = LocalDateTime.now();
// Use 'sssss' - too many seconds digits will throw IllegalArgumentException from
// DateTimeFormatter
// Note: The pattern 'sssss' passes the SAFE_DATE_FORMAT_PATTERN but fails
// DateTimeFormatter.ofPattern()
String result = invokeProcessCustomDateFormat("sssss", now);
assertTrue(
result.startsWith("[invalid format:"),
"Invalid pattern should return error message: " + result);
}
}
@Nested
@DisplayName("Escape Sequence Tests")
class EscapeSequenceTests {
@Test
@DisplayName("Should convert @@ to literal @")
void testDoubleAtEscape() throws Exception {
String result =
invokeProcessStampText("Email: test@@example.com", 1, 1, "test.pdf", null);
assertEquals("Email: test@example.com", result);
}
@Test
@DisplayName("Should preserve @@ before variable")
void testEscapeBeforeVariable() throws Exception {
String result = invokeProcessStampText("@@date is @date", 1, 1, "test.pdf", null);
// @@date should become @date, and @date should be replaced with actual date
assertTrue(result.startsWith("@date is "), "Should start with literal @date");
assertTrue(
result.matches("@date is \\d{4}-\\d{2}-\\d{2}"),
"Should have date after: " + result);
}
@Test
@DisplayName("Should handle multiple escape sequences")
void testMultipleEscapes() throws Exception {
String result = invokeProcessStampText("@@one @@two @@three", 1, 1, "test.pdf", null);
assertEquals("@one @two @three", result);
}
@Test
@DisplayName("Should handle escape at end of string")
void testEscapeAtEnd() throws Exception {
String result = invokeProcessStampText("Contact: user@@", 1, 1, "test.pdf", null);
assertEquals("Contact: user@", result);
}
}
@Nested
@DisplayName("Document Metadata Tests")
class DocumentMetadataTests {
@Test
@DisplayName("Should replace @author with document author")
void testAuthorReplacement() throws Exception {
PDDocument doc = new PDDocument();
PDDocumentInformation info = new PDDocumentInformation();
info.setAuthor("John Doe");
doc.setDocumentInformation(info);
try {
String result = invokeProcessStampText("Author: @author", 1, 1, "test.pdf", doc);
assertEquals("Author: John Doe", result);
} finally {
doc.close();
}
}
@Test
@DisplayName("Should replace @title with document title")
void testTitleReplacement() throws Exception {
PDDocument doc = new PDDocument();
PDDocumentInformation info = new PDDocumentInformation();
info.setTitle("My Document Title");
doc.setDocumentInformation(info);
try {
String result = invokeProcessStampText("Title: @title", 1, 1, "test.pdf", doc);
assertEquals("Title: My Document Title", result);
} finally {
doc.close();
}
}
@Test
@DisplayName("Should replace @subject with document subject")
void testSubjectReplacement() throws Exception {
PDDocument doc = new PDDocument();
PDDocumentInformation info = new PDDocumentInformation();
info.setSubject("Important Subject");
doc.setDocumentInformation(info);
try {
String result = invokeProcessStampText("Subject: @subject", 1, 1, "test.pdf", doc);
assertEquals("Subject: Important Subject", result);
} finally {
doc.close();
}
}
@Test
@DisplayName("Should handle null metadata gracefully")
void testNullMetadata() throws Exception {
PDDocument doc = new PDDocument();
// Don't set any document information
try {
String result =
invokeProcessStampText("@author @title @subject", 1, 1, "test.pdf", doc);
assertEquals(" ", result); // All should be empty strings
} finally {
doc.close();
}
}
@Test
@DisplayName("Should handle null document gracefully")
void testNullDocument() throws Exception {
String result = invokeProcessStampText("Author: @author", 1, 1, "test.pdf", null);
assertEquals("Author: ", result);
}
}
@Nested
@DisplayName("UUID Variable Tests")
class UuidTests {
@Test
@DisplayName("Should generate 8-character UUID")
void testUuidLength() throws Exception {
String result = invokeProcessStampText("ID: @uuid", 1, 1, "test.pdf", null);
// UUID format: "ID: " + 8 chars
assertEquals(12, result.length(), "Should be 'ID: ' + 8 char UUID");
}
@Test
@DisplayName("Should generate different UUIDs for each call")
void testUuidUniqueness() throws Exception {
String result1 = invokeProcessStampText("@uuid", 1, 1, "test.pdf", null);
String result2 = invokeProcessStampText("@uuid", 1, 1, "test.pdf", null);
assertNotEquals(result1, result2, "UUIDs should be unique");
}
@Test
@DisplayName("UUID should contain only hex characters")
void testUuidFormat() throws Exception {
String result = invokeProcessStampText("@uuid", 1, 1, "test.pdf", null);
assertTrue(result.matches("[0-9a-f]{8}"), "UUID should be 8 hex characters: " + result);
}
}
@Nested
@DisplayName("Edge Cases and Error Handling")
class EdgeCaseTests {
@Test
@DisplayName("Should handle null stamp text")
void testNullStampText() throws Exception {
String result = invokeProcessStampText(null, 1, 1, "test.pdf", null);
assertEquals("", result);
}
@Test
@DisplayName("Should handle empty stamp text")
void testEmptyStampText() throws Exception {
String result = invokeProcessStampText("", 1, 1, "test.pdf", null);
assertEquals("", result);
}
@Test
@DisplayName("Should handle text with no variables")
void testNoVariables() throws Exception {
String result = invokeProcessStampText("Just plain text", 1, 1, "test.pdf", null);
assertEquals("Just plain text", result);
}
@Test
@DisplayName("Should handle unknown variables")
void testUnknownVariable() throws Exception {
String result = invokeProcessStampText("@unknown_var", 1, 1, "test.pdf", null);
assertEquals("@unknown_var", result);
}
@Test
@DisplayName("Should preserve text around variables")
void testPreservesSurroundingText() throws Exception {
String result =
invokeProcessStampText("Before @page_number After", 5, 10, "test.pdf", null);
assertEquals("Before 5 After", result);
}
@Test
@DisplayName("Should handle multiple same variables")
void testMultipleSameVariables() throws Exception {
String result =
invokeProcessStampText("@page_number / @page_number", 3, 10, "test.pdf", null);
assertEquals("3 / 3", result);
}
@Test
@DisplayName("Should handle variables adjacent to each other")
void testAdjacentVariables() throws Exception {
String result = invokeProcessStampText("@page@page_number", 5, 10, "test.pdf", null);
// @page should be replaced first (it's in the order), then @page_number
// Since @page_number is longer and comes first in replace chain, should work
assertEquals("55", result);
}
}
@Nested
@DisplayName("Complex Scenario Tests")
class ComplexScenarioTests {
@Test
@DisplayName("Should handle legal footer template")
void testLegalFooterTemplate() throws Exception {
String template = "© @year - All Rights Reserved\\n@filename - Page @page_number";
String result = invokeProcessStampText(template, 3, 15, "contract.pdf", null);
int year = LocalDateTime.now().getYear();
String expected = "© " + year + " - All Rights Reserved\\ncontract - Page 3";
assertEquals(expected, result);
}
@Test
@DisplayName("Should handle Brazilian date format template")
void testBrazilianDateFormat() throws Exception {
String template = "Documento criado em @date{dd/MM/yyyy} às @time";
String result = invokeProcessStampText(template, 1, 1, "doc.pdf", null);
assertTrue(result.startsWith("Documento criado em "));
assertTrue(result.contains("/"));
assertTrue(result.contains(":"));
}
@ParameterizedTest
@CsvSource({
"'Page @page_number of @total_pages', 1, 10, 'Page 1 of 10'",
"'Page @page_number of @total_pages', 5, 20, 'Page 5 of 20'",
"'Page @page_number of @total_pages', 100, 1000, 'Page 100 of 1000'"
})
@DisplayName("Should handle page number template with various values")
void testPageNumberTemplates(String template, int page, int total, String expected)
throws Exception {
String result = invokeProcessStampText(template, page, total, "test.pdf", null);
assertEquals(expected, result);
}
}
}
@@ -33,8 +33,7 @@ public class PersistentAuditEvent {
private String principal;
private String type;
@Column(columnDefinition = "text")
private String data; // JSON blob
@Lob private String data; // JSON blob
private Instant timestamp;
}
@@ -8,7 +8,6 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
@@ -186,39 +185,11 @@ public class SecurityConfiguration {
}
@Bean
@Order(1)
public SecurityFilterChain samlFilterChain(
HttpSecurity http,
@Lazy IPRateLimitingFilter rateLimitingFilter,
@Lazy JwtAuthenticationFilter jwtAuthenticationFilter)
throws Exception {
http.securityMatcher("/saml2/**", "/login/saml2/**");
SessionCreationPolicy sessionPolicy =
(securityProperties.isSaml2Active() && runningProOrHigher)
? SessionCreationPolicy.IF_REQUIRED
: SessionCreationPolicy.STATELESS;
return configureSecurity(http, rateLimitingFilter, jwtAuthenticationFilter, sessionPolicy);
}
@Bean
@Order(2)
public SecurityFilterChain filterChain(
HttpSecurity http,
@Lazy IPRateLimitingFilter rateLimitingFilter,
@Lazy JwtAuthenticationFilter jwtAuthenticationFilter)
throws Exception {
SessionCreationPolicy sessionPolicy = SessionCreationPolicy.STATELESS;
return configureSecurity(http, rateLimitingFilter, jwtAuthenticationFilter, sessionPolicy);
}
private SecurityFilterChain configureSecurity(
HttpSecurity http,
@Lazy IPRateLimitingFilter rateLimitingFilter,
@Lazy JwtAuthenticationFilter jwtAuthenticationFilter,
SessionCreationPolicy sessionPolicy)
throws Exception {
// Enable CORS only if we have configured origins
CorsConfigurationSource corsSource = corsConfigurationSource();
if (corsSource != null) {
@@ -262,7 +233,9 @@ public class SecurityConfiguration {
.addFilterBefore(jwtAuthenticationFilter, UserAuthenticationFilter.class);
http.sessionManagement(
sessionManagement -> sessionManagement.sessionCreationPolicy(sessionPolicy));
sessionManagement ->
sessionManagement.sessionCreationPolicy(
SessionCreationPolicy.STATELESS));
http.authenticationProvider(daoAuthenticationProvider());
http.requestCache(requestCache -> requestCache.requestCache(new NullRequestCache()));
@@ -186,13 +186,6 @@ public class AdminSettingsController {
+ HtmlUtils.htmlEscape(key)));
}
// Validate pipeline path settings
String validationError = validatePipelinePathSetting(key, value);
if (validationError != null) {
return ResponseEntity.badRequest()
.body(Map.of("error", HtmlUtils.htmlEscape(validationError)));
}
log.info("Admin updating setting: {} = {}", key, value);
GeneralUtils.saveKeyToSettings(key, value);
@@ -649,54 +642,6 @@ public class AdminSettingsController {
return true;
}
private String validatePipelinePathSetting(String key, Object value) {
// Validate pipeline path settings
if (key.startsWith("system.customPaths.pipeline.watchedFoldersDirs")
&& value instanceof java.util.List) {
@SuppressWarnings("unchecked")
java.util.List<String> paths = (java.util.List<String>) value;
// Check for empty or all-blank paths
if (paths.isEmpty()) {
return null; // Empty is OK, will use default
}
// Validate each path
java.util.Set<String> normalizedPaths = new java.util.HashSet<>();
for (String path : paths) {
if (path != null && !path.trim().isEmpty()) {
try {
java.nio.file.Path normalized =
java.nio.file.Paths.get(path.trim()).toAbsolutePath().normalize();
String normalizedStr = normalized.toString();
// Check for duplicates
if (normalizedPaths.contains(normalizedStr)) {
return "Duplicate path detected: " + path;
}
normalizedPaths.add(normalizedStr);
} catch (java.nio.file.InvalidPathException e) {
return "Invalid path: " + path + " - " + e.getMessage();
}
}
}
// Check for overlapping paths
java.util.List<String> pathList = new java.util.ArrayList<>(normalizedPaths);
for (int i = 0; i < pathList.size(); i++) {
java.nio.file.Path path1 = java.nio.file.Paths.get(pathList.get(i));
for (int j = i + 1; j < pathList.size(); j++) {
java.nio.file.Path path2 = java.nio.file.Paths.get(pathList.get(j));
if (path1.startsWith(path2) || path2.startsWith(path1)) {
return "Overlapping paths detected: " + path1 + " and " + path2;
}
}
}
}
return null; // Valid
}
private Object getSettingByKey(String key) {
if (key == null || key.trim().isEmpty()) {
return null;
@@ -286,10 +286,7 @@ public class AuthController {
log.debug("Token refreshed for user: {}", username);
return ResponseEntity.ok(
Map.of(
"user", buildUserResponse(user),
"session", Map.of("access_token", newToken, "expires_in", 3600)));
return ResponseEntity.ok(Map.of("access_token", newToken, "expires_in", 3600));
} catch (Exception e) {
log.error("Token refresh error", e);
@@ -87,6 +87,7 @@ public class User implements UserDetails, Serializable {
@ElementCollection
@MapKeyColumn(name = "setting_key")
@Lob
@Column(name = "setting_value", columnDefinition = "text")
@CollectionTable(name = "user_settings", joinColumns = @JoinColumn(name = "user_id"))
@JsonIgnore
@@ -0,0 +1,135 @@
package stirling.software.proprietary.security.saml2;
import java.util.HashMap;
import java.util.Map;
import org.springframework.security.saml2.provider.service.authentication.Saml2PostAuthenticationRequest;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestRepository;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import stirling.software.proprietary.security.service.JwtServiceInterface;
@Slf4j
public class JwtSaml2AuthenticationRequestRepository
implements Saml2AuthenticationRequestRepository<Saml2PostAuthenticationRequest> {
private final Map<String, String> tokenStore;
private final JwtServiceInterface jwtService;
private final RelyingPartyRegistrationRepository relyingPartyRegistrationRepository;
private static final String SAML_REQUEST_TOKEN = "stirling_saml_request_token";
public JwtSaml2AuthenticationRequestRepository(
Map<String, String> tokenStore,
JwtServiceInterface jwtService,
RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) {
this.tokenStore = tokenStore;
this.jwtService = jwtService;
this.relyingPartyRegistrationRepository = relyingPartyRegistrationRepository;
}
@Override
public void saveAuthenticationRequest(
Saml2PostAuthenticationRequest authRequest,
HttpServletRequest request,
HttpServletResponse response) {
if (!jwtService.isJwtEnabled()) {
log.debug("V2 is not enabled, skipping SAMLRequest token storage");
return;
}
if (authRequest == null) {
removeAuthenticationRequest(request, response);
return;
}
Map<String, Object> claims = serializeSamlRequest(authRequest);
String token = jwtService.generateToken("", claims);
String relayState = authRequest.getRelayState();
tokenStore.put(relayState, token);
request.setAttribute(SAML_REQUEST_TOKEN, relayState);
response.addHeader(SAML_REQUEST_TOKEN, relayState);
log.debug("Saved SAMLRequest token with RelayState: {}", relayState);
}
@Override
public Saml2PostAuthenticationRequest loadAuthenticationRequest(HttpServletRequest request) {
String token = extractTokenFromStore(request);
if (token == null) {
log.debug("No SAMLResponse token found in RelayState");
return null;
}
Map<String, Object> claims = jwtService.extractClaims(token);
return deserializeSamlRequest(claims);
}
@Override
public Saml2PostAuthenticationRequest removeAuthenticationRequest(
HttpServletRequest request, HttpServletResponse response) {
Saml2PostAuthenticationRequest authRequest = loadAuthenticationRequest(request);
String relayStateId = request.getParameter("RelayState");
if (relayStateId != null) {
tokenStore.remove(relayStateId);
log.debug("Removed SAMLRequest token for RelayState ID: {}", relayStateId);
}
return authRequest;
}
private String extractTokenFromStore(HttpServletRequest request) {
String authnRequestId = request.getParameter("RelayState");
if (authnRequestId != null && !authnRequestId.isEmpty()) {
String token = tokenStore.get(authnRequestId);
if (token != null) {
tokenStore.remove(authnRequestId);
log.debug("Retrieved SAMLRequest token for RelayState ID: {}", authnRequestId);
return token;
} else {
log.warn("No SAMLRequest token found for RelayState ID: {}", authnRequestId);
}
}
return null;
}
private Map<String, Object> serializeSamlRequest(Saml2PostAuthenticationRequest authRequest) {
Map<String, Object> claims = new HashMap<>();
claims.put("id", authRequest.getId());
claims.put("relyingPartyRegistrationId", authRequest.getRelyingPartyRegistrationId());
claims.put("authenticationRequestUri", authRequest.getAuthenticationRequestUri());
claims.put("samlRequest", authRequest.getSamlRequest());
claims.put("relayState", authRequest.getRelayState());
return claims;
}
private Saml2PostAuthenticationRequest deserializeSamlRequest(Map<String, Object> claims) {
String relyingPartyRegistrationId = (String) claims.get("relyingPartyRegistrationId");
RelyingPartyRegistration relyingPartyRegistration =
relyingPartyRegistrationRepository.findByRegistrationId(relyingPartyRegistrationId);
if (relyingPartyRegistration == null) {
return null;
}
return Saml2PostAuthenticationRequest.withRelyingPartyRegistration(relyingPartyRegistration)
.id((String) claims.get("id"))
.authenticationRequestUri((String) claims.get("authenticationRequestUri"))
.samlRequest((String) claims.get("samlRequest"))
.relayState((String) claims.get("relayState"))
.build();
}
}
@@ -3,6 +3,7 @@ package stirling.software.proprietary.security.saml2;
import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import org.opensaml.saml.saml2.core.AuthnRequest;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@@ -11,10 +12,12 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.security.saml2.core.Saml2X509Credential;
import org.springframework.security.saml2.core.Saml2X509Credential.Saml2X509CredentialType;
import org.springframework.security.saml2.provider.service.authentication.Saml2PostAuthenticationRequest;
import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestRepository;
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver;
import jakarta.servlet.http.HttpServletRequest;
@@ -24,6 +27,7 @@ import lombok.extern.slf4j.Slf4j;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.model.ApplicationProperties.Security.SAML2;
import stirling.software.proprietary.security.service.JwtServiceInterface;
@Configuration
@Slf4j
@@ -149,10 +153,22 @@ public class Saml2Configuration {
return new InMemoryRelyingPartyRegistrationRepository(rp);
}
@Bean
@ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true")
public Saml2AuthenticationRequestRepository<Saml2PostAuthenticationRequest>
saml2AuthenticationRequestRepository(
JwtServiceInterface jwtService,
RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) {
return new JwtSaml2AuthenticationRequestRepository(
new ConcurrentHashMap<>(), jwtService, relyingPartyRegistrationRepository);
}
@Bean
@ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true")
public OpenSaml4AuthenticationRequestResolver authenticationRequestResolver(
RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) {
RelyingPartyRegistrationRepository relyingPartyRegistrationRepository,
Saml2AuthenticationRequestRepository<Saml2PostAuthenticationRequest>
saml2AuthenticationRequestRepository) {
OpenSaml4AuthenticationRequestResolver resolver =
new OpenSaml4AuthenticationRequestResolver(relyingPartyRegistrationRepository);
@@ -160,10 +176,30 @@ public class Saml2Configuration {
customizer -> {
HttpServletRequest request = customizer.getRequest();
AuthnRequest authnRequest = customizer.getAuthnRequest();
Saml2PostAuthenticationRequest saml2AuthenticationRequest =
saml2AuthenticationRequestRepository.loadAuthenticationRequest(request);
// Generate a unique AuthnRequest ID for each SAML request
authnRequest.setID("ARQ" + UUID.randomUUID().toString().substring(1));
if (saml2AuthenticationRequest != null) {
String sessionId = request.getSession(false).getId();
log.debug(
"Retrieving SAML 2 authentication request ID from the current HTTP session {}",
sessionId);
String authenticationRequestId = saml2AuthenticationRequest.getId();
if (!authenticationRequestId.isBlank()) {
authnRequest.setID(authenticationRequestId);
} else {
log.warn(
"No authentication request found for HTTP session {}. Generating new ID",
sessionId);
authnRequest.setID("ARQ" + UUID.randomUUID().toString().substring(1));
}
} else {
log.debug("Generating new authentication request ID");
authnRequest.setID("ARQ" + UUID.randomUUID().toString().substring(1));
}
logAuthnRequestDetails(authnRequest);
logHttpRequestDetails(request);
});
@@ -67,7 +67,6 @@ public class UserService implements UserServiceInterface {
private final ApplicationProperties.Security.OAUTH2 oAuth2;
@Transactional
public void processSSOPostLogin(
String username,
String ssoProviderId,
@@ -182,9 +182,8 @@ class AuthControllerLoginTest {
mockMvc.perform(post("/api/v1/auth/refresh"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.user").exists())
.andExpect(jsonPath("$.session.access_token").value("new-token"))
.andExpect(jsonPath("$.session.expires_in").value(3600));
.andExpect(jsonPath("$.access_token").value("new-token"))
.andExpect(jsonPath("$.expires_in").value(3600));
}
@Test
@@ -0,0 +1,243 @@
package stirling.software.proprietary.security.saml2;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.NullAndEmptySource;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.saml2.provider.service.authentication.Saml2PostAuthenticationRequest;
import org.springframework.security.saml2.provider.service.registration.AssertingPartyMetadata;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import stirling.software.proprietary.security.service.JwtServiceInterface;
@ExtendWith(MockitoExtension.class)
class JwtSaml2AuthenticationRequestRepositoryTest {
private static final String SAML_REQUEST_TOKEN = "stirling_saml_request_token";
private Map<String, String> tokenStore;
@Mock private JwtServiceInterface jwtService;
@Mock private RelyingPartyRegistrationRepository relyingPartyRegistrationRepository;
private JwtSaml2AuthenticationRequestRepository jwtSaml2AuthenticationRequestRepository;
@BeforeEach
void setUp() {
tokenStore = new ConcurrentHashMap<>();
jwtSaml2AuthenticationRequestRepository =
new JwtSaml2AuthenticationRequestRepository(
tokenStore, jwtService, relyingPartyRegistrationRepository);
}
@Test
void saveAuthenticationRequest() {
var authRequest = mock(Saml2PostAuthenticationRequest.class);
var request = mock(MockHttpServletRequest.class);
var response = mock(MockHttpServletResponse.class);
String token = "testToken";
String id = "testId";
String relayState = "testRelayState";
String authnRequestUri = "example.com/authnRequest";
String samlRequest = "testSamlRequest";
String relyingPartyRegistrationId = "stirling-pdf";
when(jwtService.isJwtEnabled()).thenReturn(true);
when(authRequest.getRelayState()).thenReturn(relayState);
when(authRequest.getId()).thenReturn(id);
when(authRequest.getAuthenticationRequestUri()).thenReturn(authnRequestUri);
when(authRequest.getSamlRequest()).thenReturn(samlRequest);
when(authRequest.getRelyingPartyRegistrationId()).thenReturn(relyingPartyRegistrationId);
when(jwtService.generateToken(eq(""), anyMap())).thenReturn(token);
jwtSaml2AuthenticationRequestRepository.saveAuthenticationRequest(
authRequest, request, response);
verify(request).setAttribute(SAML_REQUEST_TOKEN, relayState);
verify(response).addHeader(SAML_REQUEST_TOKEN, relayState);
}
@Test
void saveAuthenticationRequestWithNullRequest() {
var request = mock(MockHttpServletRequest.class);
var response = mock(MockHttpServletResponse.class);
jwtSaml2AuthenticationRequestRepository.saveAuthenticationRequest(null, request, response);
assertTrue(tokenStore.isEmpty());
}
@Test
void loadAuthenticationRequest() {
var request = mock(MockHttpServletRequest.class);
var relyingPartyRegistration = mock(RelyingPartyRegistration.class);
var assertingPartyMetadata = mock(AssertingPartyMetadata.class);
String relayState = "testRelayState";
String token = "testToken";
Map<String, Object> claims =
Map.of(
"id", "testId",
"relyingPartyRegistrationId", "stirling-pdf",
"authenticationRequestUri", "example.com/authnRequest",
"samlRequest", "testSamlRequest",
"relayState", relayState);
when(request.getParameter("RelayState")).thenReturn(relayState);
when(jwtService.extractClaims(token)).thenReturn(claims);
when(relyingPartyRegistrationRepository.findByRegistrationId("stirling-pdf"))
.thenReturn(relyingPartyRegistration);
when(relyingPartyRegistration.getRegistrationId()).thenReturn("stirling-pdf");
when(relyingPartyRegistration.getAssertingPartyMetadata())
.thenReturn(assertingPartyMetadata);
when(assertingPartyMetadata.getSingleSignOnServiceLocation())
.thenReturn("https://example.com/sso");
tokenStore.put(relayState, token);
var result = jwtSaml2AuthenticationRequestRepository.loadAuthenticationRequest(request);
assertNotNull(result);
assertFalse(tokenStore.containsKey(relayState));
}
@ParameterizedTest
@NullAndEmptySource
void loadAuthenticationRequestWithInvalidRelayState(String relayState) {
var request = mock(MockHttpServletRequest.class);
when(request.getParameter("RelayState")).thenReturn(relayState);
var result = jwtSaml2AuthenticationRequestRepository.loadAuthenticationRequest(request);
assertNull(result);
}
@Test
void loadAuthenticationRequestWithNonExistentToken() {
var request = mock(MockHttpServletRequest.class);
when(request.getParameter("RelayState")).thenReturn("nonExistentRelayState");
var result = jwtSaml2AuthenticationRequestRepository.loadAuthenticationRequest(request);
assertNull(result);
}
@Test
void loadAuthenticationRequestWithNullRelyingPartyRegistration() {
var request = mock(MockHttpServletRequest.class);
String relayState = "testRelayState";
String token = "testToken";
Map<String, Object> claims =
Map.of(
"id", "testId",
"relyingPartyRegistrationId", "stirling-pdf",
"authenticationRequestUri", "example.com/authnRequest",
"samlRequest", "testSamlRequest",
"relayState", relayState);
when(request.getParameter("RelayState")).thenReturn(relayState);
when(jwtService.extractClaims(token)).thenReturn(claims);
when(relyingPartyRegistrationRepository.findByRegistrationId("stirling-pdf"))
.thenReturn(null);
tokenStore.put(relayState, token);
var result = jwtSaml2AuthenticationRequestRepository.loadAuthenticationRequest(request);
assertNull(result);
}
@Test
void removeAuthenticationRequest() {
var request = mock(HttpServletRequest.class);
var response = mock(HttpServletResponse.class);
var relyingPartyRegistration = mock(RelyingPartyRegistration.class);
var assertingPartyMetadata = mock(AssertingPartyMetadata.class);
String relayState = "testRelayState";
String token = "testToken";
Map<String, Object> claims =
Map.of(
"id", "testId",
"relyingPartyRegistrationId", "stirling-pdf",
"authenticationRequestUri", "example.com/authnRequest",
"samlRequest", "testSamlRequest",
"relayState", relayState);
when(request.getParameter("RelayState")).thenReturn(relayState);
when(jwtService.extractClaims(token)).thenReturn(claims);
when(relyingPartyRegistrationRepository.findByRegistrationId("stirling-pdf"))
.thenReturn(relyingPartyRegistration);
when(relyingPartyRegistration.getRegistrationId()).thenReturn("stirling-pdf");
when(relyingPartyRegistration.getAssertingPartyMetadata())
.thenReturn(assertingPartyMetadata);
when(assertingPartyMetadata.getSingleSignOnServiceLocation())
.thenReturn("https://example.com/sso");
tokenStore.put(relayState, token);
var result =
jwtSaml2AuthenticationRequestRepository.removeAuthenticationRequest(
request, response);
assertNotNull(result);
assertFalse(tokenStore.containsKey(relayState));
}
@Test
void removeAuthenticationRequestWithNullRelayState() {
var request = mock(HttpServletRequest.class);
var response = mock(HttpServletResponse.class);
when(request.getParameter("RelayState")).thenReturn(null);
var result =
jwtSaml2AuthenticationRequestRepository.removeAuthenticationRequest(
request, response);
assertNull(result);
}
@Test
void removeAuthenticationRequestWithNonExistentToken() {
var request = mock(HttpServletRequest.class);
var response = mock(HttpServletResponse.class);
when(request.getParameter("RelayState")).thenReturn("nonExistentRelayState");
var result =
jwtSaml2AuthenticationRequestRepository.removeAuthenticationRequest(
request, response);
assertNull(result);
}
@Test
void removeAuthenticationRequestWithOnlyRelayState() {
var request = mock(HttpServletRequest.class);
var response = mock(HttpServletResponse.class);
String relayState = "testRelayState";
when(request.getParameter("RelayState")).thenReturn(relayState);
var result =
jwtSaml2AuthenticationRequestRepository.removeAuthenticationRequest(
request, response);
assertNull(result);
assertFalse(tokenStore.containsKey(relayState));
}
}
+1 -1
View File
@@ -67,7 +67,7 @@ springBoot {
allprojects {
group = 'stirling.software'
version = '2.4.3'
version = '2.4.1'
configurations.configureEach {
exclude group: 'commons-logging', module: 'commons-logging'
+916 -595
View File
File diff suppressed because it is too large Load Diff
+1 -36
View File
@@ -439,9 +439,7 @@ fullscreen = "ملء الشاشة"
[settings.general.updates]
title = "تحديثات البرنامج"
description = "التحقق من التحديثات وعرض معلومات الإصدار"
currentBackendVersion = "Current Backend Version"
currentFrontendVersion = "Current Frontend Version"
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
currentVersion = "الإصدار الحالي"
latestVersion = "أحدث إصدار"
checkForUpdates = "التحقق من التحديثات"
viewDetails = "عرض التفاصيل"
@@ -3562,31 +3560,6 @@ imageSize = "حجم الصورة"
margin = "الهامش"
positionAndFormatting = "الموضع والتنسيق"
quickPosition = "اختر موضعًا على الصفحة لوضع الختم."
clickToExpand = "Click to expand"
customDateDesc = "Custom format"
dateDesc = "Current date"
dateTimeVars = "Date & Time"
datetimeDesc = "Date and time combined"
dynamicVariables = "Dynamic Variables"
examples = "Examples"
fileVars = "File Information"
filenameDesc = "Filename without extension"
filenameFullDesc = "Filename with extension"
metadataDesc = "From PDF document properties"
metadataVars = "Document Metadata"
multiLine = "multi-line"
otherVars = "Other"
pageNumberDesc = "Current page number"
pageVars = "Page Information"
preview = "Preview:"
selectTemplate = "Select a template..."
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
timeDesc = "Current time"
totalPagesDesc = "Total number of pages"
useTemplate = "Use Template"
uuidDesc = "Short unique identifier (8 chars)"
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
yearMonthDayDesc = "Individual date parts"
[AddStampRequest.error]
failed = "حدث خطأ أثناء إضافة الختم إلى ملف PDF."
@@ -3594,14 +3567,6 @@ failed = "حدث خطأ أثناء إضافة الختم إلى ملف PDF."
[AddStampRequest.results]
title = "نتائج إضافة الختم"
[AddStampRequest.template]
custom = "Custom"
dateHeader = "Date Header"
draftWatermark = "Draft Watermark"
europeanDate = "European Date"
pageNumberFooter = "Page Number Footer"
timestamp = "Timestamp"
[removeImagePdf]
tags = "إزالة الصورة,عمليات الصفحة,الخلفية,جانب الخادم"
+1 -36
View File
@@ -439,9 +439,7 @@ fullscreen = "Tam ekran"
[settings.general.updates]
title = "Proqram yeniləmələri"
description = "Yeniləmələri yoxlayın və versiya məlumatlarını görün"
currentBackendVersion = "Current Backend Version"
currentFrontendVersion = "Current Frontend Version"
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
currentVersion = "Cari versiya"
latestVersion = "Ən son versiya"
checkForUpdates = "Yeniləmələri yoxla"
viewDetails = "Ətraflı bax"
@@ -3562,31 +3560,6 @@ imageSize = "Şəkil ölçüsü"
margin = "Kənar boşluğu"
positionAndFormatting = "Mövqe və formatlama"
quickPosition = "Möhürü yerləşdirmək üçün səhifədə bir mövqe seçin."
clickToExpand = "Click to expand"
customDateDesc = "Custom format"
dateDesc = "Current date"
dateTimeVars = "Date & Time"
datetimeDesc = "Date and time combined"
dynamicVariables = "Dynamic Variables"
examples = "Examples"
fileVars = "File Information"
filenameDesc = "Filename without extension"
filenameFullDesc = "Filename with extension"
metadataDesc = "From PDF document properties"
metadataVars = "Document Metadata"
multiLine = "multi-line"
otherVars = "Other"
pageNumberDesc = "Current page number"
pageVars = "Page Information"
preview = "Preview:"
selectTemplate = "Select a template..."
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
timeDesc = "Current time"
totalPagesDesc = "Total number of pages"
useTemplate = "Use Template"
uuidDesc = "Short unique identifier (8 chars)"
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
yearMonthDayDesc = "Individual date parts"
[AddStampRequest.error]
failed = "PDF-ə möhür əlavə edilərkən xəta baş verdi."
@@ -3594,14 +3567,6 @@ failed = "PDF-ə möhür əlavə edilərkən xəta baş verdi."
[AddStampRequest.results]
title = "Möhür nəticələri"
[AddStampRequest.template]
custom = "Custom"
dateHeader = "Date Header"
draftWatermark = "Draft Watermark"
europeanDate = "European Date"
pageNumberFooter = "Page Number Footer"
timestamp = "Timestamp"
[removeImagePdf]
tags = "Şəkil Sil,Səhifə Əməliyyatları,Back end,server-tərəf"
+1 -36
View File
@@ -439,9 +439,7 @@ fullscreen = "Цял екран"
[settings.general.updates]
title = "Актуализации на софтуера"
description = "Проверете за актуализации и вижте информация за версията"
currentBackendVersion = "Current Backend Version"
currentFrontendVersion = "Current Frontend Version"
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
currentVersion = "Текуща версия"
latestVersion = "Последна версия"
checkForUpdates = "Провери за актуализации"
viewDetails = "Виж подробности"
@@ -3562,31 +3560,6 @@ imageSize = "Размер на изображението"
margin = "Отстъп"
positionAndFormatting = "Позиция и форматиране"
quickPosition = "Изберете позиция на страницата, където да поставите печата."
clickToExpand = "Click to expand"
customDateDesc = "Custom format"
dateDesc = "Current date"
dateTimeVars = "Date & Time"
datetimeDesc = "Date and time combined"
dynamicVariables = "Dynamic Variables"
examples = "Examples"
fileVars = "File Information"
filenameDesc = "Filename without extension"
filenameFullDesc = "Filename with extension"
metadataDesc = "From PDF document properties"
metadataVars = "Document Metadata"
multiLine = "multi-line"
otherVars = "Other"
pageNumberDesc = "Current page number"
pageVars = "Page Information"
preview = "Preview:"
selectTemplate = "Select a template..."
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
timeDesc = "Current time"
totalPagesDesc = "Total number of pages"
useTemplate = "Use Template"
uuidDesc = "Short unique identifier (8 chars)"
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
yearMonthDayDesc = "Individual date parts"
[AddStampRequest.error]
failed = "Възникна грешка при добавяне на печат към PDF."
@@ -3594,14 +3567,6 @@ failed = "Възникна грешка при добавяне на печат
[AddStampRequest.results]
title = "Резултати от печата"
[AddStampRequest.template]
custom = "Custom"
dateHeader = "Date Header"
draftWatermark = "Draft Watermark"
europeanDate = "European Date"
pageNumberFooter = "Page Number Footer"
timestamp = "Timestamp"
[removeImagePdf]
tags = "Премахване на изображение, операции на страници, админ страна, страна на сървъра"
+1 -36
View File
@@ -439,9 +439,7 @@ fullscreen = "བརྙན་ཤེལ་ཆ་ཚང་།"
[settings.general.updates]
title = "མཉེན་ཆས་གསར་བརྗེ།"
description = "གསར་བརྗེའི་གནས་ཚུལ་ལ་ཞིབ་བཤེར་དང་ཐོན་རིམ་གྱི་ཆ་འཕྲིན་ལ་ལྟོས།"
currentBackendVersion = "Current Backend Version"
currentFrontendVersion = "Current Frontend Version"
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
currentVersion = "ད་ལྟའི་ཐོན་རིམ།"
latestVersion = "ཐོན་རིམ་གསར་ཤོས།"
checkForUpdates = "གསར་བརྗེ་ལ་ཞིབ་བཤེར།"
viewDetails = "ཞིབ་ཕྲའི་གནས་ཚུལ་ལ་ལྟོས།"
@@ -3562,31 +3560,6 @@ imageSize = "པར་རིས་ཚད་གཞི།"
margin = "མཐའ་ཤོག"
positionAndFormatting = "ལས་གནས་དང་རྩ་སྒྲིག།"
quickPosition = "ཤོག་ངོས་སྟེང་དུ་གནས་ཡུལ་ཞིག་འདེམས་ནས་མཚོན་རྟགས་འཇོག་དགོས།"
clickToExpand = "Click to expand"
customDateDesc = "Custom format"
dateDesc = "Current date"
dateTimeVars = "Date & Time"
datetimeDesc = "Date and time combined"
dynamicVariables = "Dynamic Variables"
examples = "Examples"
fileVars = "File Information"
filenameDesc = "Filename without extension"
filenameFullDesc = "Filename with extension"
metadataDesc = "From PDF document properties"
metadataVars = "Document Metadata"
multiLine = "multi-line"
otherVars = "Other"
pageNumberDesc = "Current page number"
pageVars = "Page Information"
preview = "Preview:"
selectTemplate = "Select a template..."
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
timeDesc = "Current time"
totalPagesDesc = "Total number of pages"
useTemplate = "Use Template"
uuidDesc = "Short unique identifier (8 chars)"
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
yearMonthDayDesc = "Individual date parts"
[AddStampRequest.error]
failed = "PDF ལ་མཚོན་རྟགས་ཁ་སྣོན་བྱེད་སྐབས་ནོར་འཁྲུལ་བྱུང་ཡོད།"
@@ -3594,14 +3567,6 @@ failed = "PDF ལ་མཚོན་རྟགས་ཁ་སྣོན་བྱེ
[AddStampRequest.results]
title = "མཚོན་རྟགས་གྲུབ་འབྲས།"
[AddStampRequest.template]
custom = "Custom"
dateHeader = "Date Header"
draftWatermark = "Draft Watermark"
europeanDate = "European Date"
pageNumberFooter = "Page Number Footer"
timestamp = "Timestamp"
[removeImagePdf]
tags = "པར་རིས་མེད་པར་བཟོས་པ། ཤོག་ངོས་བཀོལ་སྤྱོད། རྒྱབ་མཇུག་། ཞབས་ཞུ་པ།"
+1 -36
View File
@@ -439,9 +439,7 @@ fullscreen = "Pantalla completa"
[settings.general.updates]
title = "Actualitzacions de programari"
description = "Comprova actualitzacions i informació de la versió"
currentBackendVersion = "Current Backend Version"
currentFrontendVersion = "Current Frontend Version"
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
currentVersion = "Versió actual"
latestVersion = "Última versió"
checkForUpdates = "Comprova actualitzacions"
viewDetails = "Veure detalls"
@@ -3562,31 +3560,6 @@ imageSize = "Mida de la imatge"
margin = "Marge"
positionAndFormatting = "Posició i format"
quickPosition = "Seleccioneu una posició a la pàgina per col·locar el segell."
clickToExpand = "Click to expand"
customDateDesc = "Custom format"
dateDesc = "Current date"
dateTimeVars = "Date & Time"
datetimeDesc = "Date and time combined"
dynamicVariables = "Dynamic Variables"
examples = "Examples"
fileVars = "File Information"
filenameDesc = "Filename without extension"
filenameFullDesc = "Filename with extension"
metadataDesc = "From PDF document properties"
metadataVars = "Document Metadata"
multiLine = "multi-line"
otherVars = "Other"
pageNumberDesc = "Current page number"
pageVars = "Page Information"
preview = "Preview:"
selectTemplate = "Select a template..."
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
timeDesc = "Current time"
totalPagesDesc = "Total number of pages"
useTemplate = "Use Template"
uuidDesc = "Short unique identifier (8 chars)"
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
yearMonthDayDesc = "Individual date parts"
[AddStampRequest.error]
failed = "S'ha produït un error en afegir el segell al PDF."
@@ -3594,14 +3567,6 @@ failed = "S'ha produït un error en afegir el segell al PDF."
[AddStampRequest.results]
title = "Resultats del segell"
[AddStampRequest.template]
custom = "Custom"
dateHeader = "Date Header"
draftWatermark = "Draft Watermark"
europeanDate = "European Date"
pageNumberFooter = "Page Number Footer"
timestamp = "Timestamp"
[removeImagePdf]
tags = "Elimina imatge,Operacions de pàgina,Back-end,Servidor"
+1 -36
View File
@@ -439,9 +439,7 @@ fullscreen = "Celá obrazovka"
[settings.general.updates]
title = "Aktualizace softwaru"
description = "Zkontrolujte aktualizace a zobrazte informace o verzi"
currentBackendVersion = "Current Backend Version"
currentFrontendVersion = "Current Frontend Version"
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
currentVersion = "Aktuální verze"
latestVersion = "Nejnovější verze"
checkForUpdates = "Zkontrolovat aktualizace"
viewDetails = "Zobrazit podrobnosti"
@@ -3562,31 +3560,6 @@ imageSize = "Velikost obrázku"
margin = "Okraj"
positionAndFormatting = "Umístění a formátování"
quickPosition = "Vyberte na stránce pozici pro umístění razítka."
clickToExpand = "Click to expand"
customDateDesc = "Custom format"
dateDesc = "Current date"
dateTimeVars = "Date & Time"
datetimeDesc = "Date and time combined"
dynamicVariables = "Dynamic Variables"
examples = "Examples"
fileVars = "File Information"
filenameDesc = "Filename without extension"
filenameFullDesc = "Filename with extension"
metadataDesc = "From PDF document properties"
metadataVars = "Document Metadata"
multiLine = "multi-line"
otherVars = "Other"
pageNumberDesc = "Current page number"
pageVars = "Page Information"
preview = "Preview:"
selectTemplate = "Select a template..."
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
timeDesc = "Current time"
totalPagesDesc = "Total number of pages"
useTemplate = "Use Template"
uuidDesc = "Short unique identifier (8 chars)"
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
yearMonthDayDesc = "Individual date parts"
[AddStampRequest.error]
failed = "Při přidávání razítka do PDF došlo k chybě."
@@ -3594,14 +3567,6 @@ failed = "Při přidávání razítka do PDF došlo k chybě."
[AddStampRequest.results]
title = "Výsledky razítka"
[AddStampRequest.template]
custom = "Custom"
dateHeader = "Date Header"
draftWatermark = "Draft Watermark"
europeanDate = "European Date"
pageNumberFooter = "Page Number Footer"
timestamp = "Timestamp"
[removeImagePdf]
tags = "Odstranit obrázek,Operace stránek,zadní strana,serverová strana"
+1 -36
View File
@@ -439,9 +439,7 @@ fullscreen = "Fuldskærm"
[settings.general.updates]
title = "Softwareopdateringer"
description = "Søg efter opdateringer og se versionsinfo"
currentBackendVersion = "Current Backend Version"
currentFrontendVersion = "Current Frontend Version"
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
currentVersion = "Nuværende version"
latestVersion = "Seneste version"
checkForUpdates = "Søg efter opdateringer"
viewDetails = "Vis detaljer"
@@ -3562,31 +3560,6 @@ imageSize = "Billedstørrelse"
margin = "Margen"
positionAndFormatting = "Placering og formatering"
quickPosition = "Vælg en placering på siden til stemplet."
clickToExpand = "Click to expand"
customDateDesc = "Custom format"
dateDesc = "Current date"
dateTimeVars = "Date & Time"
datetimeDesc = "Date and time combined"
dynamicVariables = "Dynamic Variables"
examples = "Examples"
fileVars = "File Information"
filenameDesc = "Filename without extension"
filenameFullDesc = "Filename with extension"
metadataDesc = "From PDF document properties"
metadataVars = "Document Metadata"
multiLine = "multi-line"
otherVars = "Other"
pageNumberDesc = "Current page number"
pageVars = "Page Information"
preview = "Preview:"
selectTemplate = "Select a template..."
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
timeDesc = "Current time"
totalPagesDesc = "Total number of pages"
useTemplate = "Use Template"
uuidDesc = "Short unique identifier (8 chars)"
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
yearMonthDayDesc = "Individual date parts"
[AddStampRequest.error]
failed = "Der opstod en fejl under tilføjelse af stempel til PDF'en."
@@ -3594,14 +3567,6 @@ failed = "Der opstod en fejl under tilføjelse af stempel til PDF'en."
[AddStampRequest.results]
title = "Stempelresultater"
[AddStampRequest.template]
custom = "Custom"
dateHeader = "Date Header"
draftWatermark = "Draft Watermark"
europeanDate = "European Date"
pageNumberFooter = "Page Number Footer"
timestamp = "Timestamp"
[removeImagePdf]
tags = "Fjern Billede,Sideoperationer,Back end,server side"
+1 -36
View File
@@ -439,9 +439,7 @@ fullscreen = "Vollbild"
[settings.general.updates]
title = "Software-Updates"
description = "Nach Updates suchen und Versionsinformationen anzeigen"
currentBackendVersion = "Current Backend Version"
currentFrontendVersion = "Current Frontend Version"
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
currentVersion = "Aktuelle Version"
latestVersion = "Neueste Version"
checkForUpdates = "Nach Updates suchen"
viewDetails = "Details anzeigen"
@@ -3562,31 +3560,6 @@ imageSize = "Bildgröße"
margin = "Rand"
positionAndFormatting = "Position & Formatierung"
quickPosition = "Wählen Sie eine Position auf der Seite, um den Stempel zu platzieren."
clickToExpand = "Click to expand"
customDateDesc = "Custom format"
dateDesc = "Current date"
dateTimeVars = "Date & Time"
datetimeDesc = "Date and time combined"
dynamicVariables = "Dynamic Variables"
examples = "Examples"
fileVars = "File Information"
filenameDesc = "Filename without extension"
filenameFullDesc = "Filename with extension"
metadataDesc = "From PDF document properties"
metadataVars = "Document Metadata"
multiLine = "multi-line"
otherVars = "Other"
pageNumberDesc = "Current page number"
pageVars = "Page Information"
preview = "Preview:"
selectTemplate = "Select a template..."
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
timeDesc = "Current time"
totalPagesDesc = "Total number of pages"
useTemplate = "Use Template"
uuidDesc = "Short unique identifier (8 chars)"
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
yearMonthDayDesc = "Individual date parts"
[AddStampRequest.error]
failed = "Beim Hinzufügen des Stempels zum PDF ist ein Fehler aufgetreten."
@@ -3594,14 +3567,6 @@ failed = "Beim Hinzufügen des Stempels zum PDF ist ein Fehler aufgetreten."
[AddStampRequest.results]
title = "Stempel-Ergebnisse"
[AddStampRequest.template]
custom = "Custom"
dateHeader = "Date Header"
draftWatermark = "Draft Watermark"
europeanDate = "European Date"
pageNumberFooter = "Page Number Footer"
timestamp = "Timestamp"
[removeImagePdf]
tags = "bild entfernen,seitenoperationen,back end,server side"
+1 -36
View File
@@ -439,9 +439,7 @@ fullscreen = "Πλήρης οθόνη"
[settings.general.updates]
title = "Ενημερώσεις λογισμικού"
description = "Έλεγχος ενημερώσεων και προβολή πληροφοριών έκδοσης"
currentBackendVersion = "Current Backend Version"
currentFrontendVersion = "Current Frontend Version"
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
currentVersion = "Τρέχουσα έκδοση"
latestVersion = "Νεότερη έκδοση"
checkForUpdates = "Έλεγχος για ενημερώσεις"
viewDetails = "Προβολή λεπτομερειών"
@@ -3562,31 +3560,6 @@ imageSize = "Μέγεθος εικόνας"
margin = "Περιθώριο"
positionAndFormatting = "Θέση & μορφοποίηση"
quickPosition = "Επιλέξτε μια θέση στη σελίδα για τοποθέτηση της σφραγίδας."
clickToExpand = "Click to expand"
customDateDesc = "Custom format"
dateDesc = "Current date"
dateTimeVars = "Date & Time"
datetimeDesc = "Date and time combined"
dynamicVariables = "Dynamic Variables"
examples = "Examples"
fileVars = "File Information"
filenameDesc = "Filename without extension"
filenameFullDesc = "Filename with extension"
metadataDesc = "From PDF document properties"
metadataVars = "Document Metadata"
multiLine = "multi-line"
otherVars = "Other"
pageNumberDesc = "Current page number"
pageVars = "Page Information"
preview = "Preview:"
selectTemplate = "Select a template..."
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
timeDesc = "Current time"
totalPagesDesc = "Total number of pages"
useTemplate = "Use Template"
uuidDesc = "Short unique identifier (8 chars)"
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
yearMonthDayDesc = "Individual date parts"
[AddStampRequest.error]
failed = "Παρουσιάστηκε σφάλμα κατά την προσθήκη σφραγίδας στο PDF."
@@ -3594,14 +3567,6 @@ failed = "Παρουσιάστηκε σφάλμα κατά την προσθήκ
[AddStampRequest.results]
title = "Αποτελέσματα σφράγισης"
[AddStampRequest.template]
custom = "Custom"
dateHeader = "Date Header"
draftWatermark = "Draft Watermark"
europeanDate = "European Date"
pageNumberFooter = "Page Number Footer"
timestamp = "Timestamp"
[removeImagePdf]
tags = "αφαίρεση εικόνας,λειτουργίες σελίδας,backend,server side"
+1 -44
View File
@@ -439,9 +439,7 @@ fullscreen = "Fullscreen"
[settings.general.updates]
title = "Software Updates"
description = "Check for updates and view version information"
currentBackendVersion = "Current Backend Version"
currentFrontendVersion = "Current Frontend Version"
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
currentVersion = "Current Version"
latestVersion = "Latest Version"
checkForUpdates = "Check for Updates"
viewDetails = "View Details"
@@ -3562,39 +3560,6 @@ imageSize = "Image Size"
margin = "Margin"
positionAndFormatting = "Position & Formatting"
quickPosition = "Select a position on the page to place the stamp."
preview = "Preview:"
useTemplate = "Use Template"
selectTemplate = "Select a template..."
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
dynamicVariables = "Dynamic Variables"
clickToExpand = "Click to expand"
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
dateTimeVars = "Date & Time"
dateDesc = "Current date"
timeDesc = "Current time"
datetimeDesc = "Date and time combined"
customDateDesc = "Custom format"
yearMonthDayDesc = "Individual date parts"
pageVars = "Page Information"
pageNumberDesc = "Current page number"
totalPagesDesc = "Total number of pages"
fileVars = "File Information"
filenameDesc = "Filename without extension"
filenameFullDesc = "Filename with extension"
metadataVars = "Document Metadata"
metadataDesc = "From PDF document properties"
otherVars = "Other"
uuidDesc = "Short unique identifier (8 chars)"
examples = "Examples"
multiLine = "multi-line"
[AddStampRequest.template]
pageNumberFooter = "Page Number Footer"
dateHeader = "Date Header"
europeanDate = "European Date"
timestamp = "Timestamp"
draftWatermark = "Draft Watermark"
custom = "Custom"
[AddStampRequest.error]
failed = "An error occurred while adding stamp to the PDF."
@@ -4514,18 +4479,10 @@ description = "Configure custom file system paths for pipeline processing and ex
[admin.settings.general.customPaths.pipeline]
label = "Pipeline Directories"
[admin.settings.general.customPaths.pipeline.pipelineDir]
label = "Pipeline Directory"
description = "Base directory for pipeline resources (leave empty for default: /pipeline)"
[admin.settings.general.customPaths.pipeline.watchedFoldersDir]
label = "Watched Folders Directory"
description = "Directory where pipeline monitors for incoming PDFs (leave empty for default: /pipeline/watchedFolders)"
[admin.settings.general.customPaths.pipeline.watchedFoldersDirs]
label = "Watched Folders Directories"
description = "Directories where pipeline monitors for incoming PDFs (one per line or comma-separated; leave empty for default: /pipeline/watchedFolders)"
[admin.settings.general.customPaths.pipeline.finishedFoldersDir]
label = "Finished Folders Directory"
description = "Directory where processed PDFs are outputted (leave empty for default: /pipeline/finishedFolders)"
+1 -36
View File
@@ -439,9 +439,7 @@ fullscreen = "Pantalla completa"
[settings.general.updates]
title = "Actualizaciones de software"
description = "Compruebe actualizaciones y vea la información de versión"
currentBackendVersion = "Current Backend Version"
currentFrontendVersion = "Current Frontend Version"
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
currentVersion = "Versión actual"
latestVersion = "Última versión"
checkForUpdates = "Buscar actualizaciones"
viewDetails = "Ver detalles"
@@ -3562,31 +3560,6 @@ imageSize = "Tamaño de la imagen"
margin = "Margen"
positionAndFormatting = "Posición y formato"
quickPosition = "Selecciona una posición en la página para colocar el sello."
clickToExpand = "Click to expand"
customDateDesc = "Custom format"
dateDesc = "Current date"
dateTimeVars = "Date & Time"
datetimeDesc = "Date and time combined"
dynamicVariables = "Dynamic Variables"
examples = "Examples"
fileVars = "File Information"
filenameDesc = "Filename without extension"
filenameFullDesc = "Filename with extension"
metadataDesc = "From PDF document properties"
metadataVars = "Document Metadata"
multiLine = "multi-line"
otherVars = "Other"
pageNumberDesc = "Current page number"
pageVars = "Page Information"
preview = "Preview:"
selectTemplate = "Select a template..."
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
timeDesc = "Current time"
totalPagesDesc = "Total number of pages"
useTemplate = "Use Template"
uuidDesc = "Short unique identifier (8 chars)"
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
yearMonthDayDesc = "Individual date parts"
[AddStampRequest.error]
failed = "Se produjo un error al añadir el sello al PDF."
@@ -3594,14 +3567,6 @@ failed = "Se produjo un error al añadir el sello al PDF."
[AddStampRequest.results]
title = "Resultados del sello"
[AddStampRequest.template]
custom = "Custom"
dateHeader = "Date Header"
draftWatermark = "Draft Watermark"
europeanDate = "European Date"
pageNumberFooter = "Page Number Footer"
timestamp = "Timestamp"
[removeImagePdf]
tags = "Eliminar imagen,Operaciones de página,Back end,Backend"
+1 -36
View File
@@ -439,9 +439,7 @@ fullscreen = "Pantaila osoa"
[settings.general.updates]
title = "Software eguneratzeak"
description = "Egiaztatu eguneratzeak eta ikusi bertsio-informazioa"
currentBackendVersion = "Current Backend Version"
currentFrontendVersion = "Current Frontend Version"
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
currentVersion = "Uneko bertsioa"
latestVersion = "Azken bertsioa"
checkForUpdates = "Egiaztatu eguneratzeak"
viewDetails = "Xehetasunak ikusi"
@@ -3562,31 +3560,6 @@ imageSize = "Irudiaren tamaina"
margin = "Marjina"
positionAndFormatting = "Kokapena eta formatua"
quickPosition = "Hautatu orrian zigiloa kokatzeko posizio bat."
clickToExpand = "Click to expand"
customDateDesc = "Custom format"
dateDesc = "Current date"
dateTimeVars = "Date & Time"
datetimeDesc = "Date and time combined"
dynamicVariables = "Dynamic Variables"
examples = "Examples"
fileVars = "File Information"
filenameDesc = "Filename without extension"
filenameFullDesc = "Filename with extension"
metadataDesc = "From PDF document properties"
metadataVars = "Document Metadata"
multiLine = "multi-line"
otherVars = "Other"
pageNumberDesc = "Current page number"
pageVars = "Page Information"
preview = "Preview:"
selectTemplate = "Select a template..."
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
timeDesc = "Current time"
totalPagesDesc = "Total number of pages"
useTemplate = "Use Template"
uuidDesc = "Short unique identifier (8 chars)"
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
yearMonthDayDesc = "Individual date parts"
[AddStampRequest.error]
failed = "Errore bat gertatu da zigilua PDFari gehitzean."
@@ -3594,14 +3567,6 @@ failed = "Errore bat gertatu da zigilua PDFari gehitzean."
[AddStampRequest.results]
title = "Zigiluaren emaitzak"
[AddStampRequest.template]
custom = "Custom"
dateHeader = "Date Header"
draftWatermark = "Draft Watermark"
europeanDate = "European Date"
pageNumberFooter = "Page Number Footer"
timestamp = "Timestamp"
[removeImagePdf]
tags = "Irudia kendu,Orrialde eragiketak,Back end,server side"
+1 -36
View File
@@ -439,9 +439,7 @@ fullscreen = "تمام‌صفحه"
[settings.general.updates]
title = "به‌روزرسانی نرم‌افزار"
description = "بررسی به‌روزرسانی و مشاهده اطلاعات نسخه"
currentBackendVersion = "Current Backend Version"
currentFrontendVersion = "Current Frontend Version"
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
currentVersion = "نسخه فعلی"
latestVersion = "آخرین نسخه"
checkForUpdates = "بررسی به‌روزرسانی"
viewDetails = "مشاهده جزئیات"
@@ -3562,31 +3560,6 @@ imageSize = "اندازه تصویر"
margin = "حاشیه"
positionAndFormatting = "موقعیت و قالب‌بندی"
quickPosition = "یک موقعیت روی صفحه برای قرار دادن مهر انتخاب کنید."
clickToExpand = "Click to expand"
customDateDesc = "Custom format"
dateDesc = "Current date"
dateTimeVars = "Date & Time"
datetimeDesc = "Date and time combined"
dynamicVariables = "Dynamic Variables"
examples = "Examples"
fileVars = "File Information"
filenameDesc = "Filename without extension"
filenameFullDesc = "Filename with extension"
metadataDesc = "From PDF document properties"
metadataVars = "Document Metadata"
multiLine = "multi-line"
otherVars = "Other"
pageNumberDesc = "Current page number"
pageVars = "Page Information"
preview = "Preview:"
selectTemplate = "Select a template..."
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
timeDesc = "Current time"
totalPagesDesc = "Total number of pages"
useTemplate = "Use Template"
uuidDesc = "Short unique identifier (8 chars)"
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
yearMonthDayDesc = "Individual date parts"
[AddStampRequest.error]
failed = "هنگام افزودن مهر به PDF خطایی رخ داد."
@@ -3594,14 +3567,6 @@ failed = "هنگام افزودن مهر به PDF خطایی رخ داد."
[AddStampRequest.results]
title = "نتایج مهر"
[AddStampRequest.template]
custom = "Custom"
dateHeader = "Date Header"
draftWatermark = "Draft Watermark"
europeanDate = "European Date"
pageNumberFooter = "Page Number Footer"
timestamp = "Timestamp"
[removeImagePdf]
tags = "حذف تصویر، عملیات صفحه، سرور"
+1 -36
View File
@@ -439,9 +439,7 @@ fullscreen = "Plein écran"
[settings.general.updates]
title = "Mises à jour logicielles"
description = "Rechercher des mises à jour et voir les informations de version"
currentBackendVersion = "Current Backend Version"
currentFrontendVersion = "Current Frontend Version"
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
currentVersion = "Version actuelle"
latestVersion = "Dernière version"
checkForUpdates = "Rechercher des mises à jour"
viewDetails = "Voir les détails"
@@ -3562,31 +3560,6 @@ imageSize = "Taille de l’image"
margin = "Marge"
positionAndFormatting = "Position et mise en forme"
quickPosition = "Sélectionnez une position sur la page pour placer le tampon."
clickToExpand = "Click to expand"
customDateDesc = "Custom format"
dateDesc = "Current date"
dateTimeVars = "Date & Time"
datetimeDesc = "Date and time combined"
dynamicVariables = "Dynamic Variables"
examples = "Examples"
fileVars = "File Information"
filenameDesc = "Filename without extension"
filenameFullDesc = "Filename with extension"
metadataDesc = "From PDF document properties"
metadataVars = "Document Metadata"
multiLine = "multi-line"
otherVars = "Other"
pageNumberDesc = "Current page number"
pageVars = "Page Information"
preview = "Preview:"
selectTemplate = "Select a template..."
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
timeDesc = "Current time"
totalPagesDesc = "Total number of pages"
useTemplate = "Use Template"
uuidDesc = "Short unique identifier (8 chars)"
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
yearMonthDayDesc = "Individual date parts"
[AddStampRequest.error]
failed = "Une erreur s’est produite lors de l’ajout du tampon au PDF."
@@ -3594,14 +3567,6 @@ failed = "Une erreur s’est produite lors de l’ajout du tampon au PDF."
[AddStampRequest.results]
title = "Résultats du tampon"
[AddStampRequest.template]
custom = "Custom"
dateHeader = "Date Header"
draftWatermark = "Draft Watermark"
europeanDate = "European Date"
pageNumberFooter = "Page Number Footer"
timestamp = "Timestamp"
[removeImagePdf]
tags = "Images,Remove Image,Page operations,Back end,server side"
+1 -36
View File
@@ -439,9 +439,7 @@ fullscreen = "Lánscáileán"
[settings.general.updates]
title = "Nuashonruithe Bogearraí"
description = "Seiceáil le haghaidh nuashonruithe agus féach faisnéis leagain"
currentBackendVersion = "Current Backend Version"
currentFrontendVersion = "Current Frontend Version"
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
currentVersion = "Leagan Reatha"
latestVersion = "Leagan is Déanaí"
checkForUpdates = "Seiceáil le haghaidh Nuashonruithe"
viewDetails = "Féach Sonraí"
@@ -3562,31 +3560,6 @@ imageSize = "Méid Íomhá"
margin = "Imeall"
positionAndFormatting = "Suíomh & Formáidiú"
quickPosition = "Roghnaigh suíomh ar an leathanach le stampa a chur."
clickToExpand = "Click to expand"
customDateDesc = "Custom format"
dateDesc = "Current date"
dateTimeVars = "Date & Time"
datetimeDesc = "Date and time combined"
dynamicVariables = "Dynamic Variables"
examples = "Examples"
fileVars = "File Information"
filenameDesc = "Filename without extension"
filenameFullDesc = "Filename with extension"
metadataDesc = "From PDF document properties"
metadataVars = "Document Metadata"
multiLine = "multi-line"
otherVars = "Other"
pageNumberDesc = "Current page number"
pageVars = "Page Information"
preview = "Preview:"
selectTemplate = "Select a template..."
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
timeDesc = "Current time"
totalPagesDesc = "Total number of pages"
useTemplate = "Use Template"
uuidDesc = "Short unique identifier (8 chars)"
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
yearMonthDayDesc = "Individual date parts"
[AddStampRequest.error]
failed = "Tharla earráid agus stampa á chur leis an PDF."
@@ -3594,14 +3567,6 @@ failed = "Tharla earráid agus stampa á chur leis an PDF."
[AddStampRequest.results]
title = "Torthaí Stampa"
[AddStampRequest.template]
custom = "Custom"
dateHeader = "Date Header"
draftWatermark = "Draft Watermark"
europeanDate = "European Date"
pageNumberFooter = "Page Number Footer"
timestamp = "Timestamp"
[removeImagePdf]
tags = "Bain Íomhá, Oibríochtaí Leathanaigh, Cúl, taobh an fhreastalaí"
+1 -36
View File
@@ -439,9 +439,7 @@ fullscreen = "फुलस्क्रीन"
[settings.general.updates]
title = "सॉफ़्टवेयर अपडेट्स"
description = "अपडेट्स जाँचें और वर्ज़न जानकारी देखें"
currentBackendVersion = "Current Backend Version"
currentFrontendVersion = "Current Frontend Version"
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
currentVersion = "वर्तमान संस्करण"
latestVersion = "नवीनतम संस्करण"
checkForUpdates = "अपडेट्स जाँचें"
viewDetails = "विवरण देखें"
@@ -3562,31 +3560,6 @@ imageSize = "छवि आकार"
margin = "हाशिया"
positionAndFormatting = "स्थिति और फ़ॉर्मैटिंग"
quickPosition = "मुहर रखने के लिए पृष्ठ पर एक स्थान चुनें।"
clickToExpand = "Click to expand"
customDateDesc = "Custom format"
dateDesc = "Current date"
dateTimeVars = "Date & Time"
datetimeDesc = "Date and time combined"
dynamicVariables = "Dynamic Variables"
examples = "Examples"
fileVars = "File Information"
filenameDesc = "Filename without extension"
filenameFullDesc = "Filename with extension"
metadataDesc = "From PDF document properties"
metadataVars = "Document Metadata"
multiLine = "multi-line"
otherVars = "Other"
pageNumberDesc = "Current page number"
pageVars = "Page Information"
preview = "Preview:"
selectTemplate = "Select a template..."
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
timeDesc = "Current time"
totalPagesDesc = "Total number of pages"
useTemplate = "Use Template"
uuidDesc = "Short unique identifier (8 chars)"
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
yearMonthDayDesc = "Individual date parts"
[AddStampRequest.error]
failed = "PDF में मुहर जोड़ते समय त्रुटि हुई।"
@@ -3594,14 +3567,6 @@ failed = "PDF में मुहर जोड़ते समय त्रु
[AddStampRequest.results]
title = "मुहर के परिणाम"
[AddStampRequest.template]
custom = "Custom"
dateHeader = "Date Header"
draftWatermark = "Draft Watermark"
europeanDate = "European Date"
pageNumberFooter = "Page Number Footer"
timestamp = "Timestamp"
[removeImagePdf]
tags = "छवि हटाएं,पृष्ठ कार्य,बैक एंड,सर्वर साइड"
+1 -36
View File
@@ -439,9 +439,7 @@ fullscreen = "Cijeli zaslon"
[settings.general.updates]
title = "Ažuriranja softvera"
description = "Provjerite ažuriranja i pogledajte informacije o verziji"
currentBackendVersion = "Current Backend Version"
currentFrontendVersion = "Current Frontend Version"
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
currentVersion = "Trenutačna verzija"
latestVersion = "Najnovija verzija"
checkForUpdates = "Provjeri ažuriranja"
viewDetails = "Prikaži detalje"
@@ -3562,31 +3560,6 @@ imageSize = "Veličina slike"
margin = "Margina"
positionAndFormatting = "Položaj i oblikovanje"
quickPosition = "Odaberite položaj na stranici za postavljanje pečata."
clickToExpand = "Click to expand"
customDateDesc = "Custom format"
dateDesc = "Current date"
dateTimeVars = "Date & Time"
datetimeDesc = "Date and time combined"
dynamicVariables = "Dynamic Variables"
examples = "Examples"
fileVars = "File Information"
filenameDesc = "Filename without extension"
filenameFullDesc = "Filename with extension"
metadataDesc = "From PDF document properties"
metadataVars = "Document Metadata"
multiLine = "multi-line"
otherVars = "Other"
pageNumberDesc = "Current page number"
pageVars = "Page Information"
preview = "Preview:"
selectTemplate = "Select a template..."
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
timeDesc = "Current time"
totalPagesDesc = "Total number of pages"
useTemplate = "Use Template"
uuidDesc = "Short unique identifier (8 chars)"
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
yearMonthDayDesc = "Individual date parts"
[AddStampRequest.error]
failed = "Došlo je do pogreške pri dodavanju pečata u PDF."
@@ -3594,14 +3567,6 @@ failed = "Došlo je do pogreške pri dodavanju pečata u PDF."
[AddStampRequest.results]
title = "Rezultati pečata"
[AddStampRequest.template]
custom = "Custom"
dateHeader = "Date Header"
draftWatermark = "Draft Watermark"
europeanDate = "European Date"
pageNumberFooter = "Page Number Footer"
timestamp = "Timestamp"
[removeImagePdf]
tags = "Ukloni sliku, Rad sa stranicama, Back end, server strana"
+1 -36
View File
@@ -439,9 +439,7 @@ fullscreen = "Teljes képernyő"
[settings.general.updates]
title = "Szoftverfrissítések"
description = "Frissítések keresése és verzióinformációk megtekintése"
currentBackendVersion = "Current Backend Version"
currentFrontendVersion = "Current Frontend Version"
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
currentVersion = "Jelenlegi verzió"
latestVersion = "Legújabb verzió"
checkForUpdates = "Frissítések keresése"
viewDetails = "Részletek megtekintése"
@@ -3562,31 +3560,6 @@ imageSize = "Képméret"
margin = "Margó"
positionAndFormatting = "Pozíció és formázás"
quickPosition = "Válasszon pozíciót az oldalon a bélyeg elhelyezéséhez."
clickToExpand = "Click to expand"
customDateDesc = "Custom format"
dateDesc = "Current date"
dateTimeVars = "Date & Time"
datetimeDesc = "Date and time combined"
dynamicVariables = "Dynamic Variables"
examples = "Examples"
fileVars = "File Information"
filenameDesc = "Filename without extension"
filenameFullDesc = "Filename with extension"
metadataDesc = "From PDF document properties"
metadataVars = "Document Metadata"
multiLine = "multi-line"
otherVars = "Other"
pageNumberDesc = "Current page number"
pageVars = "Page Information"
preview = "Preview:"
selectTemplate = "Select a template..."
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
timeDesc = "Current time"
totalPagesDesc = "Total number of pages"
useTemplate = "Use Template"
uuidDesc = "Short unique identifier (8 chars)"
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
yearMonthDayDesc = "Individual date parts"
[AddStampRequest.error]
failed = "Hiba történt a bélyeg hozzáadása közben a PDF-hez."
@@ -3594,14 +3567,6 @@ failed = "Hiba történt a bélyeg hozzáadása közben a PDF-hez."
[AddStampRequest.results]
title = "Bélyegzés eredményei"
[AddStampRequest.template]
custom = "Custom"
dateHeader = "Date Header"
draftWatermark = "Draft Watermark"
europeanDate = "European Date"
pageNumberFooter = "Page Number Footer"
timestamp = "Timestamp"
[removeImagePdf]
tags = "Képek eltávolítása,Oldalműveletek,Backend,szerver oldali"
+1 -36
View File
@@ -439,9 +439,7 @@ fullscreen = "Layar penuh"
[settings.general.updates]
title = "Pembaruan Software"
description = "Periksa pembaruan dan lihat informasi versi"
currentBackendVersion = "Current Backend Version"
currentFrontendVersion = "Current Frontend Version"
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
currentVersion = "Versi Saat Ini"
latestVersion = "Versi Terbaru"
checkForUpdates = "Periksa Pembaruan"
viewDetails = "Lihat Detail"
@@ -3562,31 +3560,6 @@ imageSize = "Ukuran Gambar"
margin = "Margin"
positionAndFormatting = "Posisi & Pemformatan"
quickPosition = "Pilih posisi pada halaman untuk menempatkan stempel."
clickToExpand = "Click to expand"
customDateDesc = "Custom format"
dateDesc = "Current date"
dateTimeVars = "Date & Time"
datetimeDesc = "Date and time combined"
dynamicVariables = "Dynamic Variables"
examples = "Examples"
fileVars = "File Information"
filenameDesc = "Filename without extension"
filenameFullDesc = "Filename with extension"
metadataDesc = "From PDF document properties"
metadataVars = "Document Metadata"
multiLine = "multi-line"
otherVars = "Other"
pageNumberDesc = "Current page number"
pageVars = "Page Information"
preview = "Preview:"
selectTemplate = "Select a template..."
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
timeDesc = "Current time"
totalPagesDesc = "Total number of pages"
useTemplate = "Use Template"
uuidDesc = "Short unique identifier (8 chars)"
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
yearMonthDayDesc = "Individual date parts"
[AddStampRequest.error]
failed = "Terjadi kesalahan saat menambahkan stempel ke PDF."
@@ -3594,14 +3567,6 @@ failed = "Terjadi kesalahan saat menambahkan stempel ke PDF."
[AddStampRequest.results]
title = "Hasil Stempel"
[AddStampRequest.template]
custom = "Custom"
dateHeader = "Date Header"
draftWatermark = "Draft Watermark"
europeanDate = "European Date"
pageNumberFooter = "Page Number Footer"
timestamp = "Timestamp"
[removeImagePdf]
tags = "Hapus Gambar,Operasi Halaman,Backend,server side"
+1 -36
View File
@@ -439,9 +439,7 @@ fullscreen = "Schermo intero"
[settings.general.updates]
title = "Aggiornamenti software"
description = "Controlla aggiornamenti e visualizza informazioni sulla versione"
currentBackendVersion = "Current Backend Version"
currentFrontendVersion = "Current Frontend Version"
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
currentVersion = "Versione attuale"
latestVersion = "Ultima versione"
checkForUpdates = "Controlla aggiornamenti"
viewDetails = "Vedi dettagli"
@@ -3562,31 +3560,6 @@ imageSize = "Dimensione immagine"
margin = "Margine"
positionAndFormatting = "Posizione e formattazione"
quickPosition = "Seleziona una posizione sulla pagina in cui posizionare il timbro."
clickToExpand = "Click to expand"
customDateDesc = "Custom format"
dateDesc = "Current date"
dateTimeVars = "Date & Time"
datetimeDesc = "Date and time combined"
dynamicVariables = "Dynamic Variables"
examples = "Examples"
fileVars = "File Information"
filenameDesc = "Filename without extension"
filenameFullDesc = "Filename with extension"
metadataDesc = "From PDF document properties"
metadataVars = "Document Metadata"
multiLine = "multi-line"
otherVars = "Other"
pageNumberDesc = "Current page number"
pageVars = "Page Information"
preview = "Preview:"
selectTemplate = "Select a template..."
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
timeDesc = "Current time"
totalPagesDesc = "Total number of pages"
useTemplate = "Use Template"
uuidDesc = "Short unique identifier (8 chars)"
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
yearMonthDayDesc = "Individual date parts"
[AddStampRequest.error]
failed = "Si è verificato un errore durante l'aggiunta del timbro al PDF."
@@ -3594,14 +3567,6 @@ failed = "Si è verificato un errore durante l'aggiunta del timbro al PDF."
[AddStampRequest.results]
title = "Risultati timbro"
[AddStampRequest.template]
custom = "Custom"
dateHeader = "Date Header"
draftWatermark = "Draft Watermark"
europeanDate = "European Date"
pageNumberFooter = "Page Number Footer"
timestamp = "Timestamp"
[removeImagePdf]
tags = "Rimuovi immagine,operazioni sulla pagina,back-end,lato server"
+1 -36
View File
@@ -439,9 +439,7 @@ fullscreen = "フルスクリーン"
[settings.general.updates]
title = "ソフトウェア更新"
description = "更新の確認とバージョン情報の表示"
currentBackendVersion = "Current Backend Version"
currentFrontendVersion = "Current Frontend Version"
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
currentVersion = "現在のバージョン"
latestVersion = "最新バージョン"
checkForUpdates = "更新を確認"
viewDetails = "詳細を表示"
@@ -3562,31 +3560,6 @@ imageSize = "画像サイズ"
margin = "余白"
positionAndFormatting = "位置と書式"
quickPosition = "ページ上の配置位置を選択してください。"
clickToExpand = "Click to expand"
customDateDesc = "Custom format"
dateDesc = "Current date"
dateTimeVars = "Date & Time"
datetimeDesc = "Date and time combined"
dynamicVariables = "Dynamic Variables"
examples = "Examples"
fileVars = "File Information"
filenameDesc = "Filename without extension"
filenameFullDesc = "Filename with extension"
metadataDesc = "From PDF document properties"
metadataVars = "Document Metadata"
multiLine = "multi-line"
otherVars = "Other"
pageNumberDesc = "Current page number"
pageVars = "Page Information"
preview = "Preview:"
selectTemplate = "Select a template..."
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
timeDesc = "Current time"
totalPagesDesc = "Total number of pages"
useTemplate = "Use Template"
uuidDesc = "Short unique identifier (8 chars)"
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
yearMonthDayDesc = "Individual date parts"
[AddStampRequest.error]
failed = "PDF にスタンプを追加中にエラーが発生しました。"
@@ -3594,14 +3567,6 @@ failed = "PDF にスタンプを追加中にエラーが発生しました。"
[AddStampRequest.results]
title = "スタンプ結果"
[AddStampRequest.template]
custom = "Custom"
dateHeader = "Date Header"
draftWatermark = "Draft Watermark"
europeanDate = "European Date"
pageNumberFooter = "Page Number Footer"
timestamp = "Timestamp"
[removeImagePdf]
tags = "画像削除,ページ操作,バックエンド,サーバー側"
+1 -36
View File
@@ -439,9 +439,7 @@ fullscreen = "전체 화면"
[settings.general.updates]
title = "소프트웨어 업데이트"
description = "업데이트 확인 및 버전 정보 보기"
currentBackendVersion = "Current Backend Version"
currentFrontendVersion = "Current Frontend Version"
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
currentVersion = "현재 버전"
latestVersion = "최신 버전"
checkForUpdates = "업데이트 확인"
viewDetails = "자세히 보기"
@@ -3562,31 +3560,6 @@ imageSize = "이미지 크기"
margin = "여백"
positionAndFormatting = "위치 및 서식"
quickPosition = "페이지에서 스탬프 위치를 선택하세요."
clickToExpand = "Click to expand"
customDateDesc = "Custom format"
dateDesc = "Current date"
dateTimeVars = "Date & Time"
datetimeDesc = "Date and time combined"
dynamicVariables = "Dynamic Variables"
examples = "Examples"
fileVars = "File Information"
filenameDesc = "Filename without extension"
filenameFullDesc = "Filename with extension"
metadataDesc = "From PDF document properties"
metadataVars = "Document Metadata"
multiLine = "multi-line"
otherVars = "Other"
pageNumberDesc = "Current page number"
pageVars = "Page Information"
preview = "Preview:"
selectTemplate = "Select a template..."
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
timeDesc = "Current time"
totalPagesDesc = "Total number of pages"
useTemplate = "Use Template"
uuidDesc = "Short unique identifier (8 chars)"
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
yearMonthDayDesc = "Individual date parts"
[AddStampRequest.error]
failed = "PDF에 스탬프를 추가하는 중 오류가 발생했습니다."
@@ -3594,14 +3567,6 @@ failed = "PDF에 스탬프를 추가하는 중 오류가 발생했습니다."
[AddStampRequest.results]
title = "스탬프 결과"
[AddStampRequest.template]
custom = "Custom"
dateHeader = "Date Header"
draftWatermark = "Draft Watermark"
europeanDate = "European Date"
pageNumberFooter = "Page Number Footer"
timestamp = "Timestamp"
[removeImagePdf]
tags = "이미지 제거,페이지 작업,백엔드,서버 사이드"
+1 -36
View File
@@ -439,9 +439,7 @@ fullscreen = "ഫുൾസ്ക്രീൻ"
[settings.general.updates]
title = "സോഫ്റ്റ്‌വെയർ അപ്‌ഡേറ്റുകൾ"
description = "അപ്‌ഡേറ്റുകൾ പരിശോധിക്കുകയും പതിപ്പിന്റെ വിവരങ്ങൾ കാണുകയും ചെയ്യുക"
currentBackendVersion = "Current Backend Version"
currentFrontendVersion = "Current Frontend Version"
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
currentVersion = "ഇപ്പോഴത്തെ പതിപ്പ്"
latestVersion = "പുതിയ പതിപ്പ്"
checkForUpdates = "അപ്‌ഡേറ്റുകൾ പരിശോധിക്കുക"
viewDetails = "വിശദാംശങ്ങൾ കാണുക"
@@ -3562,31 +3560,6 @@ imageSize = "ഇമേജ് വലിപ്പം"
margin = "മാർജിൻ"
positionAndFormatting = "സ്ഥാനം & ഫോർമാറ്റിംഗ്"
quickPosition = "സ്റ്റാമ്പ് വയ്ക്കാൻ പേജിലെ ഒരു സ്ഥാനം തിരഞ്ഞെടുക്കുക."
clickToExpand = "Click to expand"
customDateDesc = "Custom format"
dateDesc = "Current date"
dateTimeVars = "Date & Time"
datetimeDesc = "Date and time combined"
dynamicVariables = "Dynamic Variables"
examples = "Examples"
fileVars = "File Information"
filenameDesc = "Filename without extension"
filenameFullDesc = "Filename with extension"
metadataDesc = "From PDF document properties"
metadataVars = "Document Metadata"
multiLine = "multi-line"
otherVars = "Other"
pageNumberDesc = "Current page number"
pageVars = "Page Information"
preview = "Preview:"
selectTemplate = "Select a template..."
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
timeDesc = "Current time"
totalPagesDesc = "Total number of pages"
useTemplate = "Use Template"
uuidDesc = "Short unique identifier (8 chars)"
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
yearMonthDayDesc = "Individual date parts"
[AddStampRequest.error]
failed = "PDF-ലേക്ക് സ്റ്റാമ്പ് ചേർക്കുന്നതിനിടെ പിശക് സംഭവിച്ചു."
@@ -3594,14 +3567,6 @@ failed = "PDF-ലേക്ക് സ്റ്റാമ്പ് ചേർക്
[AddStampRequest.results]
title = "സ്റ്റാമ്പ് ഫലങ്ങൾ"
[AddStampRequest.template]
custom = "Custom"
dateHeader = "Date Header"
draftWatermark = "Draft Watermark"
europeanDate = "European Date"
pageNumberFooter = "Page Number Footer"
timestamp = "Timestamp"
[removeImagePdf]
tags = "ചിത്രം നീക്കം ചെയ്യുക,പേജ് പ്രവർത്തനങ്ങൾ,ബാക്ക് എൻഡ്,സെർവർ സൈഡ്"
+1 -36
View File
@@ -439,9 +439,7 @@ fullscreen = "Volledig scherm"
[settings.general.updates]
title = "Software-updates"
description = "Controleer op updates en bekijk versie-informatie"
currentBackendVersion = "Current Backend Version"
currentFrontendVersion = "Current Frontend Version"
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
currentVersion = "Huidige versie"
latestVersion = "Laatste versie"
checkForUpdates = "Op updates controleren"
viewDetails = "Details bekijken"
@@ -3562,31 +3560,6 @@ imageSize = "Afbeeldingsgrootte"
margin = "Marge"
positionAndFormatting = "Positie & opmaak"
quickPosition = "Selecteer een positie op de pagina om de stempel te plaatsen."
clickToExpand = "Click to expand"
customDateDesc = "Custom format"
dateDesc = "Current date"
dateTimeVars = "Date & Time"
datetimeDesc = "Date and time combined"
dynamicVariables = "Dynamic Variables"
examples = "Examples"
fileVars = "File Information"
filenameDesc = "Filename without extension"
filenameFullDesc = "Filename with extension"
metadataDesc = "From PDF document properties"
metadataVars = "Document Metadata"
multiLine = "multi-line"
otherVars = "Other"
pageNumberDesc = "Current page number"
pageVars = "Page Information"
preview = "Preview:"
selectTemplate = "Select a template..."
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
timeDesc = "Current time"
totalPagesDesc = "Total number of pages"
useTemplate = "Use Template"
uuidDesc = "Short unique identifier (8 chars)"
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
yearMonthDayDesc = "Individual date parts"
[AddStampRequest.error]
failed = "Er is een fout opgetreden bij het toevoegen van een stempel aan de PDF."
@@ -3594,14 +3567,6 @@ failed = "Er is een fout opgetreden bij het toevoegen van een stempel aan de PDF
[AddStampRequest.results]
title = "Stempelresultaten"
[AddStampRequest.template]
custom = "Custom"
dateHeader = "Date Header"
draftWatermark = "Draft Watermark"
europeanDate = "European Date"
pageNumberFooter = "Page Number Footer"
timestamp = "Timestamp"
[removeImagePdf]
tags = "Afbeelding verwijderen, Paginabewerkingen, Achterkant, Serverkant"
+1 -36
View File
@@ -439,9 +439,7 @@ fullscreen = "Fullskjerm"
[settings.general.updates]
title = "Programvareoppdateringer"
description = "Se etter oppdateringer og vis versjonsinformasjon"
currentBackendVersion = "Current Backend Version"
currentFrontendVersion = "Current Frontend Version"
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
currentVersion = "Nåværende versjon"
latestVersion = "Siste versjon"
checkForUpdates = "Søk etter oppdateringer"
viewDetails = "Vis detaljer"
@@ -3562,31 +3560,6 @@ imageSize = "Bildestørrelse"
margin = "Marg"
positionAndFormatting = "Plassering og formatering"
quickPosition = "Velg en posisjon på siden for å plassere stempelet."
clickToExpand = "Click to expand"
customDateDesc = "Custom format"
dateDesc = "Current date"
dateTimeVars = "Date & Time"
datetimeDesc = "Date and time combined"
dynamicVariables = "Dynamic Variables"
examples = "Examples"
fileVars = "File Information"
filenameDesc = "Filename without extension"
filenameFullDesc = "Filename with extension"
metadataDesc = "From PDF document properties"
metadataVars = "Document Metadata"
multiLine = "multi-line"
otherVars = "Other"
pageNumberDesc = "Current page number"
pageVars = "Page Information"
preview = "Preview:"
selectTemplate = "Select a template..."
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
timeDesc = "Current time"
totalPagesDesc = "Total number of pages"
useTemplate = "Use Template"
uuidDesc = "Short unique identifier (8 chars)"
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
yearMonthDayDesc = "Individual date parts"
[AddStampRequest.error]
failed = "Det oppstod en feil under tillegg av stempel til PDF-en."
@@ -3594,14 +3567,6 @@ failed = "Det oppstod en feil under tillegg av stempel til PDF-en."
[AddStampRequest.results]
title = "Stempelresultater"
[AddStampRequest.template]
custom = "Custom"
dateHeader = "Date Header"
draftWatermark = "Draft Watermark"
europeanDate = "European Date"
pageNumberFooter = "Page Number Footer"
timestamp = "Timestamp"
[removeImagePdf]
tags = "Fjern Bilde,Sideoperasjoner,Backend,serverside"
+1 -36
View File
@@ -439,9 +439,7 @@ fullscreen = "Pełny ekran"
[settings.general.updates]
title = "Aktualizacje oprogramowania"
description = "Sprawdź aktualizacje i informacje o wersji"
currentBackendVersion = "Current Backend Version"
currentFrontendVersion = "Current Frontend Version"
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
currentVersion = "Bieżąca wersja"
latestVersion = "Najnowsza wersja"
checkForUpdates = "Sprawdź aktualizacje"
viewDetails = "Pokaż szczegóły"
@@ -3562,31 +3560,6 @@ imageSize = "Rozmiar obrazu"
margin = "Margines"
positionAndFormatting = "Pozycja i formatowanie"
quickPosition = "Wybierz pozycję na stronie, aby umieścić stempel."
clickToExpand = "Click to expand"
customDateDesc = "Custom format"
dateDesc = "Current date"
dateTimeVars = "Date & Time"
datetimeDesc = "Date and time combined"
dynamicVariables = "Dynamic Variables"
examples = "Examples"
fileVars = "File Information"
filenameDesc = "Filename without extension"
filenameFullDesc = "Filename with extension"
metadataDesc = "From PDF document properties"
metadataVars = "Document Metadata"
multiLine = "multi-line"
otherVars = "Other"
pageNumberDesc = "Current page number"
pageVars = "Page Information"
preview = "Preview:"
selectTemplate = "Select a template..."
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
timeDesc = "Current time"
totalPagesDesc = "Total number of pages"
useTemplate = "Use Template"
uuidDesc = "Short unique identifier (8 chars)"
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
yearMonthDayDesc = "Individual date parts"
[AddStampRequest.error]
failed = "Wystąpił błąd podczas dodawania stempla do PDF."
@@ -3594,14 +3567,6 @@ failed = "Wystąpił błąd podczas dodawania stempla do PDF."
[AddStampRequest.results]
title = "Wyniki dodawania stempla"
[AddStampRequest.template]
custom = "Custom"
dateHeader = "Date Header"
draftWatermark = "Draft Watermark"
europeanDate = "European Date"
pageNumberFooter = "Page Number Footer"
timestamp = "Timestamp"
[removeImagePdf]
tags = "Usuń obraz, operacje na stronie, back-end, strona serwera"
+1 -36
View File
@@ -439,9 +439,7 @@ fullscreen = "Tela cheia"
[settings.general.updates]
title = "Atualizações de software"
description = "Verifique atualizações e veja informações da versão"
currentBackendVersion = "Current Backend Version"
currentFrontendVersion = "Current Frontend Version"
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
currentVersion = "Versão atual"
latestVersion = "Última versão"
checkForUpdates = "Verificar atualizações"
viewDetails = "Ver detalhes"
@@ -3562,31 +3560,6 @@ imageSize = "Tamanho da imagem"
margin = "Margem"
positionAndFormatting = "Posição e formatação"
quickPosition = "Selecione uma posição na página para colocar o carimbo."
clickToExpand = "Click to expand"
customDateDesc = "Custom format"
dateDesc = "Current date"
dateTimeVars = "Date & Time"
datetimeDesc = "Date and time combined"
dynamicVariables = "Dynamic Variables"
examples = "Examples"
fileVars = "File Information"
filenameDesc = "Filename without extension"
filenameFullDesc = "Filename with extension"
metadataDesc = "From PDF document properties"
metadataVars = "Document Metadata"
multiLine = "multi-line"
otherVars = "Other"
pageNumberDesc = "Current page number"
pageVars = "Page Information"
preview = "Preview:"
selectTemplate = "Select a template..."
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
timeDesc = "Current time"
totalPagesDesc = "Total number of pages"
useTemplate = "Use Template"
uuidDesc = "Short unique identifier (8 chars)"
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
yearMonthDayDesc = "Individual date parts"
[AddStampRequest.error]
failed = "Ocorreu um erro ao adicionar o carimbo ao PDF."
@@ -3594,14 +3567,6 @@ failed = "Ocorreu um erro ao adicionar o carimbo ao PDF."
[AddStampRequest.results]
title = "Resultados do carimbo"
[AddStampRequest.template]
custom = "Custom"
dateHeader = "Date Header"
draftWatermark = "Draft Watermark"
europeanDate = "European Date"
pageNumberFooter = "Page Number Footer"
timestamp = "Timestamp"
[removeImagePdf]
tags = "Remover imagem,operações de página,back-end,lado do servidor"
+1 -36
View File
@@ -439,9 +439,7 @@ fullscreen = "Ecrã inteiro"
[settings.general.updates]
title = "Atualizações de software"
description = "Procurar atualizações e ver informações da versão"
currentBackendVersion = "Current Backend Version"
currentFrontendVersion = "Current Frontend Version"
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
currentVersion = "Versão atual"
latestVersion = "Última versão"
checkForUpdates = "Procurar atualizações"
viewDetails = "Ver detalhes"
@@ -3562,31 +3560,6 @@ imageSize = "Tamanho da imagem"
margin = "Margem"
positionAndFormatting = "Posição e formatação"
quickPosition = "Selecione uma posição na página para colocar o carimbo."
clickToExpand = "Click to expand"
customDateDesc = "Custom format"
dateDesc = "Current date"
dateTimeVars = "Date & Time"
datetimeDesc = "Date and time combined"
dynamicVariables = "Dynamic Variables"
examples = "Examples"
fileVars = "File Information"
filenameDesc = "Filename without extension"
filenameFullDesc = "Filename with extension"
metadataDesc = "From PDF document properties"
metadataVars = "Document Metadata"
multiLine = "multi-line"
otherVars = "Other"
pageNumberDesc = "Current page number"
pageVars = "Page Information"
preview = "Preview:"
selectTemplate = "Select a template..."
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
timeDesc = "Current time"
totalPagesDesc = "Total number of pages"
useTemplate = "Use Template"
uuidDesc = "Short unique identifier (8 chars)"
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
yearMonthDayDesc = "Individual date parts"
[AddStampRequest.error]
failed = "Ocorreu um erro ao adicionar o carimbo ao PDF."
@@ -3594,14 +3567,6 @@ failed = "Ocorreu um erro ao adicionar o carimbo ao PDF."
[AddStampRequest.results]
title = "Resultados do carimbo"
[AddStampRequest.template]
custom = "Custom"
dateHeader = "Date Header"
draftWatermark = "Draft Watermark"
europeanDate = "European Date"
pageNumberFooter = "Page Number Footer"
timestamp = "Timestamp"
[removeImagePdf]
tags = "Remover Imagem,operações de página,lado servidor"
+1 -36
View File
@@ -439,9 +439,7 @@ fullscreen = "Ecran complet"
[settings.general.updates]
title = "Actualizări software"
description = "Caută actualizări și vezi informații despre versiune"
currentBackendVersion = "Current Backend Version"
currentFrontendVersion = "Current Frontend Version"
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
currentVersion = "Versiune curentă"
latestVersion = "Ultima versiune"
checkForUpdates = "Caută actualizări"
viewDetails = "Vezi detalii"
@@ -3562,31 +3560,6 @@ imageSize = "Dimensiunea imaginii"
margin = "Margine"
positionAndFormatting = "Poziție și formatare"
quickPosition = "Selectați o poziție pe pagină pentru a plasa ștampila."
clickToExpand = "Click to expand"
customDateDesc = "Custom format"
dateDesc = "Current date"
dateTimeVars = "Date & Time"
datetimeDesc = "Date and time combined"
dynamicVariables = "Dynamic Variables"
examples = "Examples"
fileVars = "File Information"
filenameDesc = "Filename without extension"
filenameFullDesc = "Filename with extension"
metadataDesc = "From PDF document properties"
metadataVars = "Document Metadata"
multiLine = "multi-line"
otherVars = "Other"
pageNumberDesc = "Current page number"
pageVars = "Page Information"
preview = "Preview:"
selectTemplate = "Select a template..."
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
timeDesc = "Current time"
totalPagesDesc = "Total number of pages"
useTemplate = "Use Template"
uuidDesc = "Short unique identifier (8 chars)"
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
yearMonthDayDesc = "Individual date parts"
[AddStampRequest.error]
failed = "A apărut o eroare la adăugarea ștampilei în PDF."
@@ -3594,14 +3567,6 @@ failed = "A apărut o eroare la adăugarea ștampilei în PDF."
[AddStampRequest.results]
title = "Rezultatele ștampilării"
[AddStampRequest.template]
custom = "Custom"
dateHeader = "Date Header"
draftWatermark = "Draft Watermark"
europeanDate = "European Date"
pageNumberFooter = "Page Number Footer"
timestamp = "Timestamp"
[removeImagePdf]
tags = "Elimină Imagine,Operații pagină,Back end,server side"
+1 -36
View File
@@ -439,9 +439,7 @@ fullscreen = "Полноэкранный"
[settings.general.updates]
title = "Обновления ПО"
description = "Проверка обновлений и сведения о версии"
currentBackendVersion = "Current Backend Version"
currentFrontendVersion = "Current Frontend Version"
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
currentVersion = "Текущая версия"
latestVersion = "Последняя версия"
checkForUpdates = "Проверить обновления"
viewDetails = "Подробнее"
@@ -3562,31 +3560,6 @@ imageSize = "Размер изображения"
margin = "Отступ"
positionAndFormatting = "Положение и форматирование"
quickPosition = "Выберите положение на странице для размещения штампа."
clickToExpand = "Click to expand"
customDateDesc = "Custom format"
dateDesc = "Current date"
dateTimeVars = "Date & Time"
datetimeDesc = "Date and time combined"
dynamicVariables = "Dynamic Variables"
examples = "Examples"
fileVars = "File Information"
filenameDesc = "Filename without extension"
filenameFullDesc = "Filename with extension"
metadataDesc = "From PDF document properties"
metadataVars = "Document Metadata"
multiLine = "multi-line"
otherVars = "Other"
pageNumberDesc = "Current page number"
pageVars = "Page Information"
preview = "Preview:"
selectTemplate = "Select a template..."
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
timeDesc = "Current time"
totalPagesDesc = "Total number of pages"
useTemplate = "Use Template"
uuidDesc = "Short unique identifier (8 chars)"
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
yearMonthDayDesc = "Individual date parts"
[AddStampRequest.error]
failed = "Произошла ошибка при добавлении штампа в PDF."
@@ -3594,14 +3567,6 @@ failed = "Произошла ошибка при добавлении штамп
[AddStampRequest.results]
title = "Результаты штампа"
[AddStampRequest.template]
custom = "Custom"
dateHeader = "Date Header"
draftWatermark = "Draft Watermark"
europeanDate = "European Date"
pageNumberFooter = "Page Number Footer"
timestamp = "Timestamp"
[removeImagePdf]
tags = "Удаление изображения,операции со страницами,Серверная часть"
+1 -36
View File
@@ -439,9 +439,7 @@ fullscreen = "Celá obrazovka"
[settings.general.updates]
title = "Aktualizácie softvéru"
description = "Skontrolujte aktualizácie a zobrazte informácie o verzii"
currentBackendVersion = "Current Backend Version"
currentFrontendVersion = "Current Frontend Version"
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
currentVersion = "Aktuálna verzia"
latestVersion = "Najnovšia verzia"
checkForUpdates = "Skontrolovať aktualizácie"
viewDetails = "Zobraziť podrobnosti"
@@ -3562,31 +3560,6 @@ imageSize = "Veľkosť obrázka"
margin = "Okraj"
positionAndFormatting = "Poloha a formátovanie"
quickPosition = "Vyberte polohu na stránke pre umiestnenie pečiatky."
clickToExpand = "Click to expand"
customDateDesc = "Custom format"
dateDesc = "Current date"
dateTimeVars = "Date & Time"
datetimeDesc = "Date and time combined"
dynamicVariables = "Dynamic Variables"
examples = "Examples"
fileVars = "File Information"
filenameDesc = "Filename without extension"
filenameFullDesc = "Filename with extension"
metadataDesc = "From PDF document properties"
metadataVars = "Document Metadata"
multiLine = "multi-line"
otherVars = "Other"
pageNumberDesc = "Current page number"
pageVars = "Page Information"
preview = "Preview:"
selectTemplate = "Select a template..."
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
timeDesc = "Current time"
totalPagesDesc = "Total number of pages"
useTemplate = "Use Template"
uuidDesc = "Short unique identifier (8 chars)"
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
yearMonthDayDesc = "Individual date parts"
[AddStampRequest.error]
failed = "Počas pridávania pečiatky do PDF došlo k chybe."
@@ -3594,14 +3567,6 @@ failed = "Počas pridávania pečiatky do PDF došlo k chybe."
[AddStampRequest.results]
title = "Výsledky pečiatky"
[AddStampRequest.template]
custom = "Custom"
dateHeader = "Date Header"
draftWatermark = "Draft Watermark"
europeanDate = "European Date"
pageNumberFooter = "Page Number Footer"
timestamp = "Timestamp"
[removeImagePdf]
tags = "Odstrániť obrázok,Operácie so stranami,Back end,server side"
+1 -36
View File
@@ -439,9 +439,7 @@ fullscreen = "Celozaslonski"
[settings.general.updates]
title = "Posodobitve programske opreme"
description = "Preverite posodobitve in glejte informacije o različici"
currentBackendVersion = "Current Backend Version"
currentFrontendVersion = "Current Frontend Version"
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
currentVersion = "Trenutna različica"
latestVersion = "Najnovejša različica"
checkForUpdates = "Preveri posodobitve"
viewDetails = "Poglej podrobnosti"
@@ -3562,31 +3560,6 @@ imageSize = "Velikost slike"
margin = "Rob"
positionAndFormatting = "Položaj in oblikovanje"
quickPosition = "Izberite položaj na strani za postavitev žiga."
clickToExpand = "Click to expand"
customDateDesc = "Custom format"
dateDesc = "Current date"
dateTimeVars = "Date & Time"
datetimeDesc = "Date and time combined"
dynamicVariables = "Dynamic Variables"
examples = "Examples"
fileVars = "File Information"
filenameDesc = "Filename without extension"
filenameFullDesc = "Filename with extension"
metadataDesc = "From PDF document properties"
metadataVars = "Document Metadata"
multiLine = "multi-line"
otherVars = "Other"
pageNumberDesc = "Current page number"
pageVars = "Page Information"
preview = "Preview:"
selectTemplate = "Select a template..."
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
timeDesc = "Current time"
totalPagesDesc = "Total number of pages"
useTemplate = "Use Template"
uuidDesc = "Short unique identifier (8 chars)"
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
yearMonthDayDesc = "Individual date parts"
[AddStampRequest.error]
failed = "Pri dodajanju žiga v PDF je prišlo do napake."
@@ -3594,14 +3567,6 @@ failed = "Pri dodajanju žiga v PDF je prišlo do napake."
[AddStampRequest.results]
title = "Rezultati žiga"
[AddStampRequest.template]
custom = "Custom"
dateHeader = "Date Header"
draftWatermark = "Draft Watermark"
europeanDate = "European Date"
pageNumberFooter = "Page Number Footer"
timestamp = "Timestamp"
[removeImagePdf]
tags = "Odstrani sliko,operacije strani,zadnja stran,strežniška stran"
@@ -439,9 +439,7 @@ fullscreen = "Ceo ekran"
[settings.general.updates]
title = "Ažuriranja softvera"
description = "Proverite ažuriranja i pogledajte informacije o verziji"
currentBackendVersion = "Current Backend Version"
currentFrontendVersion = "Current Frontend Version"
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
currentVersion = "Trenutna verzija"
latestVersion = "Najnovija verzija"
checkForUpdates = "Proveri ažuriranja"
viewDetails = "Prikaži detalje"
@@ -3562,31 +3560,6 @@ imageSize = "Veličina slike"
margin = "Margina"
positionAndFormatting = "Pozicija i formatiranje"
quickPosition = "Izaberite poziciju na stranici za postavljanje pečata."
clickToExpand = "Click to expand"
customDateDesc = "Custom format"
dateDesc = "Current date"
dateTimeVars = "Date & Time"
datetimeDesc = "Date and time combined"
dynamicVariables = "Dynamic Variables"
examples = "Examples"
fileVars = "File Information"
filenameDesc = "Filename without extension"
filenameFullDesc = "Filename with extension"
metadataDesc = "From PDF document properties"
metadataVars = "Document Metadata"
multiLine = "multi-line"
otherVars = "Other"
pageNumberDesc = "Current page number"
pageVars = "Page Information"
preview = "Preview:"
selectTemplate = "Select a template..."
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
timeDesc = "Current time"
totalPagesDesc = "Total number of pages"
useTemplate = "Use Template"
uuidDesc = "Short unique identifier (8 chars)"
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
yearMonthDayDesc = "Individual date parts"
[AddStampRequest.error]
failed = "Došlo je do greške prilikom dodavanja pečata u PDF."
@@ -3594,14 +3567,6 @@ failed = "Došlo je do greške prilikom dodavanja pečata u PDF."
[AddStampRequest.results]
title = "Rezultati pečata"
[AddStampRequest.template]
custom = "Custom"
dateHeader = "Date Header"
draftWatermark = "Draft Watermark"
europeanDate = "European Date"
pageNumberFooter = "Page Number Footer"
timestamp = "Timestamp"
[removeImagePdf]
tags = "Ukloni sliku, Zahvati na stranici, Bekend,serverska strana"
+1 -36
View File
@@ -439,9 +439,7 @@ fullscreen = "Helskärm"
[settings.general.updates]
title = "Programuppdateringar"
description = "Sök efter uppdateringar och visa versionsinfo"
currentBackendVersion = "Current Backend Version"
currentFrontendVersion = "Current Frontend Version"
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
currentVersion = "Aktuell version"
latestVersion = "Senaste version"
checkForUpdates = "Sök efter uppdateringar"
viewDetails = "Visa detaljer"
@@ -3562,31 +3560,6 @@ imageSize = "Bildstorlek"
margin = "Marginal"
positionAndFormatting = "Position och formatering"
quickPosition = "Välj en position på sidan för att placera stämpeln."
clickToExpand = "Click to expand"
customDateDesc = "Custom format"
dateDesc = "Current date"
dateTimeVars = "Date & Time"
datetimeDesc = "Date and time combined"
dynamicVariables = "Dynamic Variables"
examples = "Examples"
fileVars = "File Information"
filenameDesc = "Filename without extension"
filenameFullDesc = "Filename with extension"
metadataDesc = "From PDF document properties"
metadataVars = "Document Metadata"
multiLine = "multi-line"
otherVars = "Other"
pageNumberDesc = "Current page number"
pageVars = "Page Information"
preview = "Preview:"
selectTemplate = "Select a template..."
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
timeDesc = "Current time"
totalPagesDesc = "Total number of pages"
useTemplate = "Use Template"
uuidDesc = "Short unique identifier (8 chars)"
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
yearMonthDayDesc = "Individual date parts"
[AddStampRequest.error]
failed = "Ett fel inträffade när stämpeln skulle läggas till i PDF:en."
@@ -3594,14 +3567,6 @@ failed = "Ett fel inträffade när stämpeln skulle läggas till i PDF:en."
[AddStampRequest.results]
title = "Resultat av stämpling"
[AddStampRequest.template]
custom = "Custom"
dateHeader = "Date Header"
draftWatermark = "Draft Watermark"
europeanDate = "European Date"
pageNumberFooter = "Page Number Footer"
timestamp = "Timestamp"
[removeImagePdf]
tags = "Ta bort bild,Sidoperationer,Backend,serversida"
+1 -36
View File
@@ -439,9 +439,7 @@ fullscreen = "เต็มหน้าจอ"
[settings.general.updates]
title = "อัปเดตซอฟต์แวร์"
description = "ตรวจสอบอัปเดตและดูข้อมูลเวอร์ชัน"
currentBackendVersion = "Current Backend Version"
currentFrontendVersion = "Current Frontend Version"
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
currentVersion = "เวอร์ชันปัจจุบัน"
latestVersion = "เวอร์ชันล่าสุด"
checkForUpdates = "ตรวจสอบอัปเดต"
viewDetails = "ดูรายละเอียด"
@@ -3562,31 +3560,6 @@ imageSize = "ขนาดรูปภาพ"
margin = "ระยะขอบ"
positionAndFormatting = "ตำแหน่งและการจัดรูปแบบ"
quickPosition = "เลือกตำแหน่งบนหน้าสำหรับวางตราประทับ"
clickToExpand = "Click to expand"
customDateDesc = "Custom format"
dateDesc = "Current date"
dateTimeVars = "Date & Time"
datetimeDesc = "Date and time combined"
dynamicVariables = "Dynamic Variables"
examples = "Examples"
fileVars = "File Information"
filenameDesc = "Filename without extension"
filenameFullDesc = "Filename with extension"
metadataDesc = "From PDF document properties"
metadataVars = "Document Metadata"
multiLine = "multi-line"
otherVars = "Other"
pageNumberDesc = "Current page number"
pageVars = "Page Information"
preview = "Preview:"
selectTemplate = "Select a template..."
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
timeDesc = "Current time"
totalPagesDesc = "Total number of pages"
useTemplate = "Use Template"
uuidDesc = "Short unique identifier (8 chars)"
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
yearMonthDayDesc = "Individual date parts"
[AddStampRequest.error]
failed = "เกิดข้อผิดพลาดขณะเพิ่มตราประทับลงใน PDF"
@@ -3594,14 +3567,6 @@ failed = "เกิดข้อผิดพลาดขณะเพิ่มต
[AddStampRequest.results]
title = "ผลการประทับตรา"
[AddStampRequest.template]
custom = "Custom"
dateHeader = "Date Header"
draftWatermark = "Draft Watermark"
europeanDate = "European Date"
pageNumberFooter = "Page Number Footer"
timestamp = "Timestamp"
[removeImagePdf]
tags = "ลบรูปภาพ,การจัดการหน้า,แบ็กเอนด์,ฝั่งเซิร์ฟเวอร์"
+1 -36
View File
@@ -439,9 +439,7 @@ fullscreen = "Tam ekran"
[settings.general.updates]
title = "Yazılım Güncellemeleri"
description = "Güncellemeleri kontrol edin ve sürüm bilgilerini görüntüleyin"
currentBackendVersion = "Current Backend Version"
currentFrontendVersion = "Current Frontend Version"
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
currentVersion = "Geçerli Sürüm"
latestVersion = "En Son Sürüm"
checkForUpdates = "Güncellemeleri Kontrol Et"
viewDetails = "Ayrıntıları Görüntüle"
@@ -3562,31 +3560,6 @@ imageSize = "Görüntü Boyutu"
margin = "Kenar Boşluğu"
positionAndFormatting = "Konum ve Biçimlendirme"
quickPosition = "Damgayı yerleştirmek için sayfada bir konum seçin."
clickToExpand = "Click to expand"
customDateDesc = "Custom format"
dateDesc = "Current date"
dateTimeVars = "Date & Time"
datetimeDesc = "Date and time combined"
dynamicVariables = "Dynamic Variables"
examples = "Examples"
fileVars = "File Information"
filenameDesc = "Filename without extension"
filenameFullDesc = "Filename with extension"
metadataDesc = "From PDF document properties"
metadataVars = "Document Metadata"
multiLine = "multi-line"
otherVars = "Other"
pageNumberDesc = "Current page number"
pageVars = "Page Information"
preview = "Preview:"
selectTemplate = "Select a template..."
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
timeDesc = "Current time"
totalPagesDesc = "Total number of pages"
useTemplate = "Use Template"
uuidDesc = "Short unique identifier (8 chars)"
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
yearMonthDayDesc = "Individual date parts"
[AddStampRequest.error]
failed = "PDF'ye damga eklenirken bir hata oluştu."
@@ -3594,14 +3567,6 @@ failed = "PDF'ye damga eklenirken bir hata oluştu."
[AddStampRequest.results]
title = "Damga Sonuçları"
[AddStampRequest.template]
custom = "Custom"
dateHeader = "Date Header"
draftWatermark = "Draft Watermark"
europeanDate = "European Date"
pageNumberFooter = "Page Number Footer"
timestamp = "Timestamp"
[removeImagePdf]
tags = "Resmi Kaldır,Sayfa İşlemleri,Arka uç,sunucu tarafı"
+1 -36
View File
@@ -439,9 +439,7 @@ fullscreen = "Повноекранний"
[settings.general.updates]
title = "Оновлення ПЗ"
description = "Перевіряйте оновлення та переглядайте інформацію про версію"
currentBackendVersion = "Current Backend Version"
currentFrontendVersion = "Current Frontend Version"
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
currentVersion = "Поточна версія"
latestVersion = "Остання версія"
checkForUpdates = "Перевірити оновлення"
viewDetails = "Переглянути деталі"
@@ -3562,31 +3560,6 @@ imageSize = "Розмір зображення"
margin = "Поле"
positionAndFormatting = "Позиція та форматування"
quickPosition = "Виберіть позицію на сторінці для розміщення штампа."
clickToExpand = "Click to expand"
customDateDesc = "Custom format"
dateDesc = "Current date"
dateTimeVars = "Date & Time"
datetimeDesc = "Date and time combined"
dynamicVariables = "Dynamic Variables"
examples = "Examples"
fileVars = "File Information"
filenameDesc = "Filename without extension"
filenameFullDesc = "Filename with extension"
metadataDesc = "From PDF document properties"
metadataVars = "Document Metadata"
multiLine = "multi-line"
otherVars = "Other"
pageNumberDesc = "Current page number"
pageVars = "Page Information"
preview = "Preview:"
selectTemplate = "Select a template..."
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
timeDesc = "Current time"
totalPagesDesc = "Total number of pages"
useTemplate = "Use Template"
uuidDesc = "Short unique identifier (8 chars)"
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
yearMonthDayDesc = "Individual date parts"
[AddStampRequest.error]
failed = "Під час додавання штампа до PDF сталася помилка."
@@ -3594,14 +3567,6 @@ failed = "Під час додавання штампа до PDF сталася
[AddStampRequest.results]
title = "Результати додавання штампа"
[AddStampRequest.template]
custom = "Custom"
dateHeader = "Date Header"
draftWatermark = "Draft Watermark"
europeanDate = "European Date"
pageNumberFooter = "Page Number Footer"
timestamp = "Timestamp"
[removeImagePdf]
tags = "видалення зображення,операції зі сторінками,серверна частина"
+1 -36
View File
@@ -439,9 +439,7 @@ fullscreen = "Toàn màn hình"
[settings.general.updates]
title = "Cập nhật phần mềm"
description = "Kiểm tra cập nhật và xem thông tin phiên bản"
currentBackendVersion = "Current Backend Version"
currentFrontendVersion = "Current Frontend Version"
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
currentVersion = "Phiên bản hiện tại"
latestVersion = "Phiên bản mới nhất"
checkForUpdates = "Kiểm tra cập nhật"
viewDetails = "Xem chi tiết"
@@ -3562,31 +3560,6 @@ imageSize = "Kích thước ảnh"
margin = "Lề"
positionAndFormatting = "Vị trí & định dạng"
quickPosition = "Chọn một vị trí trên trang để đặt con dấu."
clickToExpand = "Click to expand"
customDateDesc = "Custom format"
dateDesc = "Current date"
dateTimeVars = "Date & Time"
datetimeDesc = "Date and time combined"
dynamicVariables = "Dynamic Variables"
examples = "Examples"
fileVars = "File Information"
filenameDesc = "Filename without extension"
filenameFullDesc = "Filename with extension"
metadataDesc = "From PDF document properties"
metadataVars = "Document Metadata"
multiLine = "multi-line"
otherVars = "Other"
pageNumberDesc = "Current page number"
pageVars = "Page Information"
preview = "Preview:"
selectTemplate = "Select a template..."
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
timeDesc = "Current time"
totalPagesDesc = "Total number of pages"
useTemplate = "Use Template"
uuidDesc = "Short unique identifier (8 chars)"
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
yearMonthDayDesc = "Individual date parts"
[AddStampRequest.error]
failed = "Đã xảy ra lỗi khi thêm con dấu vào PDF."
@@ -3594,14 +3567,6 @@ failed = "Đã xảy ra lỗi khi thêm con dấu vào PDF."
[AddStampRequest.results]
title = "Kết quả đóng dấu"
[AddStampRequest.template]
custom = "Custom"
dateHeader = "Date Header"
draftWatermark = "Draft Watermark"
europeanDate = "European Date"
pageNumberFooter = "Page Number Footer"
timestamp = "Timestamp"
[removeImagePdf]
tags = "Xóa ảnh,Thao tác trang,Back end,phía máy chủ"
+1 -36
View File
@@ -439,9 +439,7 @@ fullscreen = "全屏"
[settings.general.updates]
title = "软件更新"
description = "检查更新并查看版本信息"
currentBackendVersion = "Current Backend Version"
currentFrontendVersion = "Current Frontend Version"
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
currentVersion = "当前版本"
latestVersion = "最新版本"
checkForUpdates = "检查更新"
viewDetails = "查看详情"
@@ -3562,31 +3560,6 @@ imageSize = "图像大小"
margin = "边距"
positionAndFormatting = "位置与格式"
quickPosition = "选择页面上的位置以放置印章。"
clickToExpand = "Click to expand"
customDateDesc = "Custom format"
dateDesc = "Current date"
dateTimeVars = "Date & Time"
datetimeDesc = "Date and time combined"
dynamicVariables = "Dynamic Variables"
examples = "Examples"
fileVars = "File Information"
filenameDesc = "Filename without extension"
filenameFullDesc = "Filename with extension"
metadataDesc = "From PDF document properties"
metadataVars = "Document Metadata"
multiLine = "multi-line"
otherVars = "Other"
pageNumberDesc = "Current page number"
pageVars = "Page Information"
preview = "Preview:"
selectTemplate = "Select a template..."
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
timeDesc = "Current time"
totalPagesDesc = "Total number of pages"
useTemplate = "Use Template"
uuidDesc = "Short unique identifier (8 chars)"
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
yearMonthDayDesc = "Individual date parts"
[AddStampRequest.error]
failed = "将印章添加到 PDF 时发生错误。"
@@ -3594,14 +3567,6 @@ failed = "将印章添加到 PDF 时发生错误。"
[AddStampRequest.results]
title = "印章结果"
[AddStampRequest.template]
custom = "Custom"
dateHeader = "Date Header"
draftWatermark = "Draft Watermark"
europeanDate = "European Date"
pageNumberFooter = "Page Number Footer"
timestamp = "Timestamp"
[removeImagePdf]
tags = "པར་རིས་སུབ་པ།,ཤོག་ངོས་བཀོལ་སྤྱོད།,རྒྱབ་ངོས།,ཞབས་ཞུ་ཕྱོགས།"
+1 -36
View File
@@ -439,9 +439,7 @@ fullscreen = "全屏"
[settings.general.updates]
title = "软件更新"
description = "检查更新并查看版本信息"
currentBackendVersion = "Current Backend Version"
currentFrontendVersion = "Current Frontend Version"
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
currentVersion = "当前版本"
latestVersion = "最新版本"
checkForUpdates = "检查更新"
viewDetails = "查看详情"
@@ -3562,31 +3560,6 @@ imageSize = "图像大小"
margin = "边距"
positionAndFormatting = "位置与格式"
quickPosition = "选择页面上的一个位置以放置图章。"
clickToExpand = "Click to expand"
customDateDesc = "Custom format"
dateDesc = "Current date"
dateTimeVars = "Date & Time"
datetimeDesc = "Date and time combined"
dynamicVariables = "Dynamic Variables"
examples = "Examples"
fileVars = "File Information"
filenameDesc = "Filename without extension"
filenameFullDesc = "Filename with extension"
metadataDesc = "From PDF document properties"
metadataVars = "Document Metadata"
multiLine = "multi-line"
otherVars = "Other"
pageNumberDesc = "Current page number"
pageVars = "Page Information"
preview = "Preview:"
selectTemplate = "Select a template..."
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
timeDesc = "Current time"
totalPagesDesc = "Total number of pages"
useTemplate = "Use Template"
uuidDesc = "Short unique identifier (8 chars)"
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
yearMonthDayDesc = "Individual date parts"
[AddStampRequest.error]
failed = "向 PDF 添加图章时发生错误。"
@@ -3594,14 +3567,6 @@ failed = "向 PDF 添加图章时发生错误。"
[AddStampRequest.results]
title = "图章结果"
[AddStampRequest.template]
custom = "Custom"
dateHeader = "Date Header"
draftWatermark = "Draft Watermark"
europeanDate = "European Date"
pageNumberFooter = "Page Number Footer"
timestamp = "Timestamp"
[removeImagePdf]
tags = "删除图像, 页面操作, 后端, 服务端"
+1 -36
View File
@@ -439,9 +439,7 @@ fullscreen = "全螢幕"
[settings.general.updates]
title = "軟體更新"
description = "檢查更新並檢視版本資訊"
currentBackendVersion = "Current Backend Version"
currentFrontendVersion = "Current Frontend Version"
versionMismatch = "Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version."
currentVersion = "目前版本"
latestVersion = "最新版本"
checkForUpdates = "檢查更新"
viewDetails = "檢視詳細資料"
@@ -3562,31 +3560,6 @@ imageSize = "影像大小"
margin = "邊距"
positionAndFormatting = "位置與格式"
quickPosition = "選擇頁面上的位置以放置印章。"
clickToExpand = "Click to expand"
customDateDesc = "Custom format"
dateDesc = "Current date"
dateTimeVars = "Date & Time"
datetimeDesc = "Date and time combined"
dynamicVariables = "Dynamic Variables"
examples = "Examples"
fileVars = "File Information"
filenameDesc = "Filename without extension"
filenameFullDesc = "Filename with extension"
metadataDesc = "From PDF document properties"
metadataVars = "Document Metadata"
multiLine = "multi-line"
otherVars = "Other"
pageNumberDesc = "Current page number"
pageVars = "Page Information"
preview = "Preview:"
selectTemplate = "Select a template..."
stampTextDescription = "Use dynamic variables below. Use @@ for literal @. Use \\n for new lines."
timeDesc = "Current time"
totalPagesDesc = "Total number of pages"
useTemplate = "Use Template"
uuidDesc = "Short unique identifier (8 chars)"
variablesHelp = "Click on any variable to insert it into your stamp text. Use @@ for literal @."
yearMonthDayDesc = "Individual date parts"
[AddStampRequest.error]
failed = "將印章加入 PDF 時發生錯誤。"
@@ -3594,14 +3567,6 @@ failed = "將印章加入 PDF 時發生錯誤。"
[AddStampRequest.results]
title = "蓋章結果"
[AddStampRequest.template]
custom = "Custom"
dateHeader = "Date Header"
draftWatermark = "Draft Watermark"
europeanDate = "European Date"
pageNumberFooter = "Page Number Footer"
timestamp = "Timestamp"
[removeImagePdf]
tags = "移除圖片,頁面操作,後端,伺服器端"
-114
View File
@@ -11,11 +11,8 @@ use rand::distributions::Alphanumeric;
const STORE_FILE: &str = "connection.json";
const USER_INFO_KEY: &str = "user_info";
const TOKENS_STORE_FILE: &str = "tokens.json";
const REFRESH_TOKEN_STORE_KEY: &str = "refresh_token";
const KEYRING_SERVICE: &str = "stirling-pdf";
const KEYRING_TOKEN_KEY: &str = "auth-token";
const KEYRING_REFRESH_TOKEN_KEY: &str = "refresh-token";
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct UserInfo {
@@ -34,11 +31,6 @@ fn get_keyring_entry() -> Result<Entry, String> {
Ok(entry)
}
fn get_refresh_token_keyring_entry() -> Result<Entry, String> {
Entry::new(KEYRING_SERVICE, KEYRING_REFRESH_TOKEN_KEY)
.map_err(|e| format!("Failed to access keyring: {}", e))
}
#[tauri::command]
pub async fn save_auth_token(_app_handle: AppHandle, token: String) -> Result<(), String> {
let trimmed = token.trim();
@@ -109,112 +101,6 @@ pub async fn clear_auth_token(_app_handle: AppHandle) -> Result<(), String> {
}
}
#[tauri::command]
pub async fn save_refresh_token(app_handle: AppHandle, token: String) -> Result<(), String> {
log::info!("Saving refresh token - trying keyring first");
let entry = get_refresh_token_keyring_entry()?;
// Try keyring (works in production with code signing)
match entry.set_password(&token) {
Ok(_) => {
// Verify it persists (fails in unsigned dev builds)
match entry.get_password() {
Ok(saved) if saved == token => {
log::info!("✅ Refresh token saved to keyring (production mode)");
return Ok(());
}
_ => {
log::info!("Keyring doesn't persist - using Tauri Store fallback (dev mode)");
}
}
}
Err(e) => {
log::info!("Keyring failed: {} - using Tauri Store fallback", e);
}
}
// Fallback to Tauri Store (dev mode without code signing)
let store = app_handle
.store(TOKENS_STORE_FILE)
.map_err(|e| format!("Failed to access tokens store: {}", e))?;
store.set(
REFRESH_TOKEN_STORE_KEY,
serde_json::to_value(&token)
.map_err(|e| format!("Failed to serialize token: {}", e))?,
);
store
.save()
.map_err(|e| format!("Failed to save tokens store: {}", e))?;
log::info!("✅ Refresh token saved to Tauri Store (fallback)");
Ok(())
}
#[tauri::command]
pub async fn get_refresh_token(app_handle: AppHandle) -> Result<Option<String>, String> {
// Try keyring first (production)
let entry = get_refresh_token_keyring_entry()?;
match entry.get_password() {
Ok(token) => {
log::info!("✅ Refresh token retrieved from keyring");
return Ok(Some(token));
}
Err(keyring::Error::NoEntry) => {
log::debug!("No token in keyring, trying Tauri Store");
}
Err(e) => {
log::warn!("Keyring error: {} - trying Tauri Store", e);
}
}
// Fallback to Tauri Store (dev)
let store = app_handle
.store(TOKENS_STORE_FILE)
.map_err(|e| format!("Failed to access tokens store: {}", e))?;
let token: Option<String> = store
.get(REFRESH_TOKEN_STORE_KEY)
.and_then(|v| serde_json::from_value(v.clone()).ok());
if token.is_some() {
log::info!("✅ Refresh token retrieved from Tauri Store");
} else {
log::info!("No refresh token found");
}
Ok(token)
}
#[tauri::command]
pub async fn clear_refresh_token(app_handle: AppHandle) -> Result<(), String> {
log::info!("Clearing refresh token from all storage");
// Clear from keyring
let entry = get_refresh_token_keyring_entry()?;
match entry.delete_credential() {
Ok(_) => log::info!("Cleared from keyring"),
Err(keyring::Error::NoEntry) => log::debug!("Not in keyring"),
Err(e) => log::warn!("Keyring clear error: {}", e),
}
// Clear from Tauri Store
let store = app_handle
.store(TOKENS_STORE_FILE)
.map_err(|e| format!("Failed to access tokens store: {}", e))?;
store.delete(REFRESH_TOKEN_STORE_KEY);
store
.save()
.map_err(|e| format!("Failed to save tokens store: {}", e))?;
log::info!("✅ Refresh token cleared");
Ok(())
}
#[tauri::command]
pub async fn save_user_info(
app_handle: AppHandle,
-3
View File
@@ -14,14 +14,11 @@ pub use connection::{
};
pub use auth::{
clear_auth_token,
clear_refresh_token,
clear_user_info,
get_auth_token,
get_refresh_token,
get_user_info,
login,
save_auth_token,
save_refresh_token,
save_user_info,
start_oauth_login,
};
-6
View File
@@ -9,20 +9,17 @@ use commands::{
cleanup_backend,
clear_auth_token,
clear_opened_files,
clear_refresh_token,
clear_user_info,
is_default_pdf_handler,
get_auth_token,
get_backend_port,
get_connection_config,
get_opened_files,
get_refresh_token,
get_user_info,
is_first_launch,
login,
reset_setup_completion,
save_auth_token,
save_refresh_token,
save_user_info,
set_connection_mode,
set_as_default_pdf_handler,
@@ -146,9 +143,6 @@ pub fn run() {
save_auth_token,
get_auth_token,
clear_auth_token,
save_refresh_token,
get_refresh_token,
clear_refresh_token,
save_user_info,
get_user_info,
clear_user_info,
@@ -11,6 +11,8 @@ pub enum ConnectionMode {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerConfig {
pub url: String,
#[serde(default)]
pub allow_invalid_certs: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"productName": "Stirling-PDF",
"version": "2.4.3",
"version": "2.4.1",
"identifier": "stirling.pdf.dev",
"build": {
"frontendDist": "../dist",
@@ -141,15 +141,14 @@ export default function Workbench() {
);
case "pageEditor":
return (
<div style={{ position: 'relative', flex: '1 1 0', height: 0 }}>
<>
<PageEditor
onFunctionsReady={setPageEditorFunctions}
/>
{pageEditorFunctions && (
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, zIndex: 100 }}>
<PageEditorControls
<PageEditorControls
onClosePdf={pageEditorFunctions.closePdf}
onUndo={pageEditorFunctions.handleUndo}
onRedo={pageEditorFunctions.handleRedo}
@@ -169,9 +168,8 @@ export default function Workbench() {
splitPositions={pageEditorFunctions.splitPositions}
totalPages={pageEditorFunctions.totalPages}
/>
</div>
)}
</div>
</>
);
default:
@@ -209,10 +207,9 @@ export default function Workbench() {
{/* Main content area */}
<Box
className={`flex-1 min-h-0 z-10 ${currentView === 'pageEditor' ? 'relative flex flex-col' : `relative ${styles.workbenchScrollable}`}`}
className={`flex-1 min-h-0 relative z-10 ${styles.workbenchScrollable}`}
style={{
transition: 'opacity 0.15s ease-in-out',
...(currentView === 'pageEditor' && { height: 0 }),
}}
>
{renderMainContent()}
@@ -37,7 +37,6 @@ interface DragDropGridProps<T extends DragDropItem> {
getThumbnailData?: (itemId: string) => { src: string; rotation: number } | null;
zoomLevel?: number;
selectedFileIds?: string[];
onVisibleItemsChange?: (items: T[]) => void;
}
type DropSide = 'left' | 'right' | null;
@@ -199,7 +198,7 @@ interface DraggableItemProps<T extends DragDropItem> {
zoomLevel: number;
}
const DraggableItemInner = <T extends DragDropItem>({ item, index, itemRefs, boxSelectedPageIds, clearBoxSelection, getBoxSelection, activeId, activeDragIds, justMoved, getThumbnailData, renderItem, onUpdateDropTarget, zoomLevel }: DraggableItemProps<T>) => {
const DraggableItem = <T extends DragDropItem>({ item, index, itemRefs, boxSelectedPageIds, clearBoxSelection, getBoxSelection, activeId, activeDragIds, justMoved, getThumbnailData, renderItem, onUpdateDropTarget, zoomLevel }: DraggableItemProps<T>) => {
const isPlaceholder = Boolean(item.isPlaceholder);
const pageNumber = (item as any).pageNumber ?? index + 1;
const { attributes, listeners, setNodeRef: setDraggableRef } = useDraggable({
@@ -253,31 +252,6 @@ const DraggableItemInner = <T extends DragDropItem>({ item, index, itemRefs, box
);
};
// Memoize to prevent unnecessary re-renders and hook thrashing
const DraggableItem = React.memo(DraggableItemInner, (prevProps, nextProps) => {
// Return true to SKIP re-render (props are equal)
// Return false to RE-RENDER (props changed)
// Check if item reference or content changed (including thumbnail)
const itemChanged = prevProps.item !== nextProps.item;
// If item object reference changed, we need to re-render
if (itemChanged) {
return false; // Props changed, re-render needed
}
// Item reference is same, check other props
return (
prevProps.item.id === nextProps.item.id &&
prevProps.index === nextProps.index &&
prevProps.activeId === nextProps.activeId &&
prevProps.justMoved === nextProps.justMoved &&
prevProps.zoomLevel === nextProps.zoomLevel &&
prevProps.activeDragIds.length === nextProps.activeDragIds.length &&
prevProps.boxSelectedPageIds.length === nextProps.boxSelectedPageIds.length
);
}) as typeof DraggableItemInner;
const DragDropGrid = <T extends DragDropItem>({
items,
renderItem,
@@ -285,7 +259,6 @@ const DragDropGrid = <T extends DragDropItem>({
getThumbnailData,
zoomLevel = 1.0,
selectedFileIds,
onVisibleItemsChange,
}: DragDropGridProps<T>) => {
const itemRefs = useRef<Map<string, HTMLDivElement>>(new Map());
const containerRef = useRef<HTMLDivElement>(null);
@@ -448,21 +421,6 @@ const DragDropGrid = <T extends DragDropItem>({
overscan: OVERSCAN,
});
const virtualRows = rowVirtualizer.getVirtualItems();
useEffect(() => {
if (!onVisibleItemsChange) return;
const visibleItemsForCallback: T[] = [];
virtualRows.forEach((row) => {
const startIndex = row.index * itemsPerRow;
const endIndex = Math.min(startIndex + itemsPerRow, visibleItems.length);
visibleItemsForCallback.push(...visibleItems.slice(startIndex, endIndex));
});
onVisibleItemsChange(visibleItemsForCallback);
}, [virtualRows, visibleItems, itemsPerRow, onVisibleItemsChange]);
// Re-measure virtualizer when zoom or items per row changes
useEffect(() => {
rowVirtualizer.measure();
@@ -761,7 +719,7 @@ const DragDropGrid = <T extends DragDropItem>({
margin: '0 auto',
}}
>
{virtualRows.map((virtualRow) => {
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
const startIndex = virtualRow.index * itemsPerRow;
const endIndex = Math.min(startIndex + itemsPerRow, visibleItems.length);
const rowItems = visibleItems.slice(startIndex, endIndex);
@@ -3,7 +3,7 @@ import { Text, Center, Box, LoadingOverlay, Stack } from "@mantine/core";
import { useFileState, useFileActions } from "@app/contexts/FileContext";
import { useNavigationGuard } from "@app/contexts/NavigationContext";
import { usePageEditor } from "@app/contexts/PageEditorContext";
import { PageEditorFunctions, PDFPage } from "@app/types/pageEditor";
import { PageEditorFunctions } from "@app/types/pageEditor";
// Thumbnail generation is now handled by individual PageThumbnail components
import '@app/components/pageEditor/PageEditor.module.css';
import PageThumbnail from '@app/components/pageEditor/PageThumbnail';
@@ -23,7 +23,6 @@ import { useUndoManagerState } from "@app/components/pageEditor/hooks/useUndoMan
import { usePageSelectionManager } from "@app/components/pageEditor/hooks/usePageSelectionManager";
import { usePageEditorCommands } from "@app/components/pageEditor/hooks/useEditorCommands";
import { usePageEditorExport } from "@app/components/pageEditor/hooks/usePageEditorExport";
import { useThumbnailGeneration } from "@app/hooks/useThumbnailGeneration";
export interface PageEditorProps {
onFunctionsReady?: (functions: PageEditorFunctions) => void;
@@ -41,27 +40,7 @@ const PageEditor = ({
const { setHasUnsavedChanges } = useNavigationGuard();
// Get PageEditor coordination functions
const {
updateFileOrderFromPages,
fileOrder,
reorderedPages,
clearReorderedPages,
updateCurrentPages,
savePersistedDocument,
} = usePageEditor();
const [visiblePageIds, setVisiblePageIds] = useState<string[]>([]);
const thumbnailRequestsRef = useRef<Set<string>>(new Set());
const { requestThumbnail, getThumbnailFromCache } = useThumbnailGeneration();
const handleVisibleItemsChange = useCallback((items: PDFPage[]) => {
setVisiblePageIds(prev => {
const ids = items.map(item => item.id);
if (prev.length === ids.length && prev.every((id, index) => id === ids[index])) {
return prev;
}
return ids;
});
}, []);
const { updateFileOrderFromPages, fileOrder, reorderedPages, clearReorderedPages, updateCurrentPages } = usePageEditor();
// Zoom state management
const [zoomLevel, setZoomLevel] = useState(1.0);
@@ -170,21 +149,6 @@ const PageEditor = ({
updateCurrentPages,
});
const displayDocumentRef = useRef(displayDocument);
useEffect(() => {
displayDocumentRef.current = displayDocument;
}, [displayDocument]);
useEffect(() => {
return () => {
const doc = displayDocumentRef.current;
if (doc && doc.pages.length > 0) {
const signature = doc.pages.map(page => page.id).join(',');
savePersistedDocument(doc, signature);
}
};
}, [savePersistedDocument]);
// UI state management
const {
selectionMode, selectedPageIds, movingPage, isAnimating, splitPositions, exportLoading,
@@ -267,92 +231,6 @@ const PageEditor = ({
setSplitPositions,
});
useEffect(() => {
if (!displayDocument || visiblePageIds.length === 0) {
return;
}
const pending = thumbnailRequestsRef.current.size;
const MAX_CONCURRENT_THUMBNAILS = 12;
const available = Math.max(0, MAX_CONCURRENT_THUMBNAILS - pending);
if (available === 0) {
return;
}
const toLoad: string[] = [];
for (const pageId of visiblePageIds) {
if (toLoad.length >= available) break;
if (thumbnailRequestsRef.current.has(pageId)) continue;
const page = displayDocument.pages.find(p => p.id === pageId);
if (!page || page.thumbnail) continue;
toLoad.push(pageId);
}
if (toLoad.length === 0) return;
toLoad.forEach(pageId => {
const page = displayDocument.pages.find(p => p.id === pageId);
if (!page) return;
const cached = getThumbnailFromCache(pageId);
if (cached) {
thumbnailRequestsRef.current.add(pageId);
Promise.resolve(cached)
.then(cache => {
setEditedDocument(prev => {
if (!prev) return prev;
const pageIndex = prev.pages.findIndex(p => p.id === pageId);
if (pageIndex === -1) return prev;
// Only create new page object for the changed page, reuse rest
const updated = [...prev.pages];
updated[pageIndex] = { ...prev.pages[pageIndex], thumbnail: cache };
return { ...prev, pages: updated };
});
})
.finally(() => {
thumbnailRequestsRef.current.delete(pageId);
});
return;
}
const fileId = page.originalFileId;
if (!fileId) return;
const file = selectors.getFile(fileId);
if (!file) return;
thumbnailRequestsRef.current.add(pageId);
requestThumbnail(pageId, file, page.originalPageNumber || page.pageNumber)
.then(thumbnail => {
if (thumbnail) {
setEditedDocument(prev => {
if (!prev) return prev;
const pageIndex = prev.pages.findIndex(p => p.id === pageId);
if (pageIndex === -1) return prev;
// Only create new page object for the changed page, reuse rest
const updated = [...prev.pages];
updated[pageIndex] = { ...prev.pages[pageIndex], thumbnail };
return { ...prev, pages: updated };
});
}
})
.catch((error) => {
console.error('[Thumbnail Loading] Error:', error);
})
.finally(() => {
thumbnailRequestsRef.current.delete(pageId);
});
});
}, [
displayDocument,
visiblePageIds,
selectors,
requestThumbnail,
getThumbnailFromCache,
setEditedDocument,
]);
// Derived values for right rail and usePageEditorRightRailButtons (must be after displayDocument)
const selectedPageCount = selectedPageIds.length;
const activeFileIds = selectedFileIds;
@@ -467,17 +345,12 @@ const PageEditor = ({
const fileColorIndexMap = useFileColorMap(orderedFileIds);
return (
<div
<Box
ref={containerRef}
pos="relative"
data-scrolling-container="true"
onMouseEnter={() => setIsContainerHovered(true)}
onMouseLeave={() => setIsContainerHovered(false)}
style={{
height: '100%',
overflow: 'auto',
position: 'relative',
width: '100%',
}}
>
<LoadingOverlay visible={globalProcessing && !initialDocument} />
@@ -499,7 +372,7 @@ const PageEditor = ({
)}
{displayDocument && (
<Box ref={gridContainerRef} p={0} pt="2rem" pb="4rem" style={{ position: 'relative' }}>
<Box ref={gridContainerRef} p={0} pt="2rem" pb="15rem" style={{ position: 'relative' }}>
{/* Split Lines Overlay */}
<div
@@ -577,7 +450,6 @@ const PageEditor = ({
onReorderPages={handleReorderPages}
zoomLevel={zoomLevel}
selectedFileIds={selectedFileIds}
onVisibleItemsChange={handleVisibleItemsChange}
getThumbnailData={(pageId) => {
const page = displayDocument.pages.find(p => p.id === pageId);
if (!page?.thumbnail) return null;
@@ -596,6 +468,7 @@ const PageEditor = ({
page={page}
index={index}
totalPages={displayDocument.pages.length}
originalFile={(page as any).originalFileId ? selectors.getFile((page as any).originalFileId) : undefined}
fileColorIndex={fileColorIndex}
selectedPageIds={selectedPageIds}
selectionMode={selectionMode}
@@ -639,7 +512,7 @@ const PageEditor = ({
}}
/>
</div>
</Box>
);
};
@@ -9,6 +9,7 @@ import DeleteIcon from '@mui/icons-material/Delete';
import ContentCutIcon from '@mui/icons-material/ContentCut';
import AddIcon from '@mui/icons-material/Add';
import { PDFPage, PDFDocument } from '@app/types/pageEditor';
import { useThumbnailGeneration } from '@app/hooks/useThumbnailGeneration';
import { useFilesModalContext } from '@app/contexts/FilesModalContext';
import { getFileColorWithOpacity } from '@app/components/pageEditor/fileColors';
import styles from '@app/components/pageEditor/PageEditor.module.css';
@@ -21,6 +22,7 @@ interface PageThumbnailProps {
page: PDFPage;
index: number;
totalPages: number;
originalFile?: File;
fileColorIndex: number;
selectedPageIds: string[];
selectionMode: boolean;
@@ -53,6 +55,7 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
page,
index: _index,
totalPages,
originalFile,
fileColorIndex,
selectedPageIds,
selectionMode,
@@ -87,6 +90,7 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
const [thumbnailUrl, setThumbnailUrl] = useState<string | null>(page.thumbnail);
const elementRef = useRef<HTMLDivElement | null>(null);
const { getThumbnailFromCache, requestThumbnail} = useThumbnailGeneration();
const { openFilesModal } = useFilesModalContext();
// Check if this page is currently being dragged
@@ -111,6 +115,43 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
}
}, [page.thumbnail, thumbnailUrl]);
// Request thumbnail if missing (on-demand, virtualized approach)
useEffect(() => {
let isCancelled = false;
// If we already have a thumbnail, use it
if (page.thumbnail) {
setThumbnailUrl(page.thumbnail);
return;
}
// Check cache first
const cachedThumbnail = getThumbnailFromCache(page.id);
if (cachedThumbnail) {
setThumbnailUrl(cachedThumbnail);
return;
}
// Request thumbnail generation if we have the original file
if (originalFile) {
const pageNumber = page.originalPageNumber;
requestThumbnail(page.id, originalFile, pageNumber)
.then(thumbnail => {
if (!isCancelled && thumbnail) {
setThumbnailUrl(thumbnail);
}
})
.catch(error => {
console.warn(`Failed to generate thumbnail for ${page.id}:`, error);
});
}
return () => {
isCancelled = true;
};
}, [page.id, page.thumbnail, originalFile, getThumbnailFromCache, requestThumbnail]);
// Merge refs - combine our ref tracking with dnd-kit's ref
const mergedRef = useCallback((element: HTMLDivElement | null) => {
// Track in our refs map
@@ -748,49 +748,43 @@ export class InsertFilesCommand extends DOMCommand {
console.log('Pages:', pages.length);
console.log('ArrayBuffer size:', arrayBuffer?.byteLength || 'undefined');
try {
if (arrayBuffer && arrayBuffer.byteLength > 0) {
// Extract page numbers for all pages from this file
const pageNumbers = pages.map(page => {
const pageNumMatch = page.id.match(/-page-(\d+)$/);
return pageNumMatch ? parseInt(pageNumMatch[1]) : 1;
});
if (arrayBuffer && arrayBuffer.byteLength > 0) {
// Extract page numbers for all pages from this file
const pageNumbers = pages.map(page => {
const pageNumMatch = page.id.match(/-page-(\d+)$/);
return pageNumMatch ? parseInt(pageNumMatch[1]) : 1;
});
console.log('Generating thumbnails for page numbers:', pageNumbers);
console.log('Generating thumbnails for page numbers:', pageNumbers);
// Generate thumbnails for all pages from this file at once
const results = await thumbnailGenerationService.generateThumbnails(
fileId,
arrayBuffer,
pageNumbers,
{ scale: 0.2, quality: 0.8 }
);
// Generate thumbnails for all pages from this file at once
const results = await thumbnailGenerationService.generateThumbnails(
fileId,
arrayBuffer,
pageNumbers,
{ scale: 0.2, quality: 0.8 }
);
console.log('Thumbnail generation results:', results.length, 'thumbnails generated');
console.log('Thumbnail generation results:', results.length, 'thumbnails generated');
// Update pages with generated thumbnails
for (let i = 0; i < results.length && i < pages.length; i++) {
const result = results[i];
const page = pages[i];
// Update pages with generated thumbnails
for (let i = 0; i < results.length && i < pages.length; i++) {
const result = results[i];
const page = pages[i];
if (result.success) {
const pageIndex = updatedDocument.pages.findIndex(p => p.id === page.id);
if (pageIndex >= 0) {
updatedDocument.pages[pageIndex].thumbnail = result.thumbnail;
console.log('Updated thumbnail for page:', page.id);
}
if (result.success) {
const pageIndex = updatedDocument.pages.findIndex(p => p.id === page.id);
if (pageIndex >= 0) {
updatedDocument.pages[pageIndex].thumbnail = result.thumbnail;
console.log('Updated thumbnail for page:', page.id);
}
}
// Trigger re-render by updating the document
this.setDocument({ ...updatedDocument });
} else {
console.error('No valid ArrayBuffer found for file ID:', fileId);
}
} catch (error) {
console.error('Failed to generate thumbnails for file:', fileId, error);
} finally {
this.fileDataMap.delete(fileId);
// Trigger re-render by updating the document
this.setDocument({ ...updatedDocument });
} else {
console.error('No valid ArrayBuffer found for file ID:', fileId);
}
}
} catch (error) {
@@ -3,6 +3,6 @@ export const GRID_CONSTANTS = {
ITEM_WIDTH: '20rem', // page width
ITEM_HEIGHT: '21.5rem', // 20rem + 1.5rem gap
ITEM_GAP: '1.5rem', // gap between items
OVERSCAN_SMALL: 8, // Overscan for normal documents
OVERSCAN_LARGE: 12, // Overscan for large documents (12 rows = ~96 pages pre-rendered)
OVERSCAN_SMALL: 4, // Overscan for normal documents
OVERSCAN_LARGE: 8, // Overscan for large documents (>1000 pages)
} as const;
@@ -1,9 +1,8 @@
import { useMemo, useEffect, useState } from 'react';
import { useMemo } from 'react';
import { useFileState } from '@app/contexts/FileContext';
import { usePageEditor } from '@app/contexts/PageEditorContext';
import { PDFDocument, PDFPage } from '@app/types/pageEditor';
import { FileId } from '@app/types/file';
import { FileAnalyzer } from '@app/services/fileAnalyzer';
export interface PageDocumentHook {
document: PDFDocument | null;
@@ -17,7 +16,7 @@ export interface PageDocumentHook {
*/
export function usePageDocument(): PageDocumentHook {
const { state, selectors } = useFileState();
const { fileOrder, currentPages, persistedDocument, persistedDocumentSignature } = usePageEditor();
const { fileOrder, currentPages } = usePageEditor();
// Use PageEditorContext's fileOrder instead of FileContext's global order
// This ensures the page editor respects its own workspace ordering
@@ -59,63 +58,6 @@ export function usePageDocument(): PageDocumentHook {
const processedFilePages = primaryStirlingFileStub?.processedFile?.pages;
const processedFileTotalPages = primaryStirlingFileStub?.processedFile?.totalPages;
const [placeholderDocument, setPlaceholderDocument] = useState<PDFDocument | null>(null);
useEffect(() => {
if (!primaryFileId) {
setPlaceholderDocument(null);
return;
}
if (primaryStirlingFileStub?.processedFile) {
setPlaceholderDocument(null);
return;
}
const file = selectors.getFile(primaryFileId);
if (!file) {
setPlaceholderDocument(null);
return;
}
let canceled = false;
const loadPlaceholder = async () => {
try {
const analysis = await FileAnalyzer.quickPDFAnalysis(file);
if (canceled) return;
const totalPages = Math.max(1, analysis.pageCount || 1);
const pages: PDFPage[] = Array.from({ length: totalPages }, (_, index) => ({
id: `placeholder-${primaryFileId}-page-${index + 1}`,
pageNumber: index + 1,
thumbnail: null,
rotation: 0,
selected: false,
originalFileId: primaryFileId,
originalPageNumber: index + 1,
}));
setPlaceholderDocument({
id: `placeholder-${primaryFileId}`,
name: selectors.getStirlingFileStub(primaryFileId)?.name ?? file.name,
file,
pages,
totalPages,
});
} catch {
if (!canceled) {
setPlaceholderDocument(null);
}
}
};
loadPlaceholder();
return () => {
canceled = true;
};
}, [primaryFileId, primaryStirlingFileStub?.processedFile, selectors]);
// Compute merged document with stable signature (prevents infinite loops)
const currentPagesSignature = useMemo(() => {
return currentPages ? currentPages.map(page => page.id).join(',') : '';
@@ -124,20 +66,7 @@ export function usePageDocument(): PageDocumentHook {
const mergedPdfDocument = useMemo((): PDFDocument | null => {
if (activeFileIds.length === 0) return null;
if (
persistedDocument &&
persistedDocumentSignature &&
persistedDocumentSignature === currentPagesSignature &&
currentPagesSignature.length > 0
) {
return persistedDocument;
}
if (!primaryStirlingFileStub?.processedFile && placeholderDocument) {
return placeholderDocument;
}
const primaryFile = primaryFileId ? selectors.getFile(primaryFileId) : null;
const primaryFile = primaryFileId ? selectors.getFile(primaryFileId) : null;
// If we have file IDs but no file record, something is wrong - return null to show loading
if (!primaryStirlingFileStub) {
@@ -316,24 +245,7 @@ export function usePageDocument(): PageDocumentHook {
};
return mergedDoc;
}, [
activeFileIds,
selectedActiveFileIds,
primaryFileId,
primaryStirlingFileStub,
processedFilePages,
processedFileTotalPages,
selectors,
activeFilesSignature,
selectedFileIdsKey,
state.ui.selectedFileIds,
allFileIds,
currentPagesSignature,
currentPages,
persistedDocument,
persistedDocumentSignature,
placeholderDocument,
]);
}, [activeFileIds, selectedActiveFileIds, primaryFileId, primaryStirlingFileStub, processedFilePages, processedFileTotalPages, selectors, activeFilesSignature, selectedFileIdsKey, state.ui.selectedFileIds, allFileIds, currentPagesSignature, currentPages]);
// Large document detection for smart loading
const isVeryLargeDocument = useMemo(() => {
@@ -1,32 +1,15 @@
import React, { useState, useEffect, useMemo } from "react";
import {
Paper,
Stack,
Switch,
Text,
Tooltip,
NumberInput,
SegmentedControl,
Code,
Group,
Anchor,
ActionIcon,
Button,
Badge,
Alert,
} from "@mantine/core";
import { useTranslation } from "react-i18next";
import { usePreferences } from "@app/contexts/PreferencesContext";
import { useAppConfig } from "@app/contexts/AppConfigContext";
import type { ToolPanelMode } from "@app/constants/toolPanel";
import LocalIcon from "@app/components/shared/LocalIcon";
import { updateService, UpdateSummary } from "@app/services/updateService";
import UpdateModal from "@app/components/shared/UpdateModal";
import { getVersion } from "@tauri-apps/api/app";
import { isTauri } from "@tauri-apps/api/core";
import React, { useState, useEffect } from 'react';
import { Paper, Stack, Switch, Text, Tooltip, NumberInput, SegmentedControl, Code, Group, Anchor, ActionIcon, Button, Badge, Alert } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { usePreferences } from '@app/contexts/PreferencesContext';
import { useAppConfig } from '@app/contexts/AppConfigContext';
import type { ToolPanelMode } from '@app/constants/toolPanel';
import LocalIcon from '@app/components/shared/LocalIcon';
import { updateService, UpdateSummary } from '@app/services/updateService';
import UpdateModal from '@app/components/shared/UpdateModal';
const DEFAULT_AUTO_UNZIP_FILE_LIMIT = 4;
const BANNER_DISMISSED_KEY = "stirlingpdf_features_banner_dismissed";
const BANNER_DISMISSED_KEY = 'stirlingpdf_features_banner_dismissed';
interface GeneralSectionProps {
hideTitle?: boolean;
@@ -39,15 +22,11 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({ hideTitle = false }) =>
const [fileLimitInput, setFileLimitInput] = useState<number | string>(preferences.autoUnzipFileLimit);
const [bannerDismissed, setBannerDismissed] = useState(() => {
// Check localStorage on mount
return localStorage.getItem(BANNER_DISMISSED_KEY) === "true";
return localStorage.getItem(BANNER_DISMISSED_KEY) === 'true';
});
const [updateSummary, setUpdateSummary] = useState<UpdateSummary | null>(null);
const [updateModalOpened, setUpdateModalOpened] = useState(false);
const [checkingUpdate, setCheckingUpdate] = useState(false);
const [mismatchVersion, setMismatchVersion] = useState(false);
const isTauriApp = useMemo(() => isTauri(), []);
const [appVersion, setAppVersion] = useState<string | null>(null);
const frontendVersionLabel = appVersion ?? t("common.loading", "Loading...");
// Sync local state with preference changes
useEffect(() => {
@@ -70,7 +49,7 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({ hideTitle = false }) =>
const machineInfo = {
machineType: config.machineType,
activeSecurity: config.activeSecurity ?? false,
licenseType: config.license ?? "NORMAL",
licenseType: config.license ?? 'NORMAL',
};
const summary = await updateService.getUpdateSummary(config.appVersion, machineInfo);
@@ -89,126 +68,67 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({ hideTitle = false }) =>
setCheckingUpdate(false);
};
useEffect(() => {
if (!isTauriApp) {
setMismatchVersion(false);
return;
}
let cancelled = false;
const fetchFrontendVersion = async () => {
try {
const frontendVersion = await getVersion();
if (!cancelled) {
setAppVersion(frontendVersion);
}
} catch (error) {
console.error("[GeneralSection] Failed to fetch frontend version:", error);
}
};
fetchFrontendVersion();
return () => {
cancelled = true;
};
}, [isTauriApp]);
useEffect(() => {
if (!isTauriApp) {
return;
}
if (!appVersion || !config?.appVersion) {
setMismatchVersion(false);
return;
}
if (appVersion !== config.appVersion) {
console.warn("[GeneralSection] Mismatch between Tauri version and AppConfig version:", {
backendVersion: config.appVersion,
frontendVersion: appVersion,
});
setMismatchVersion(true);
} else {
setMismatchVersion(false);
}
}, [isTauriApp, appVersion, config?.appVersion]);
// Check if login is disabled
const loginDisabled = !config?.enableLogin;
const handleDismissBanner = () => {
setBannerDismissed(true);
localStorage.setItem(BANNER_DISMISSED_KEY, "true");
localStorage.setItem(BANNER_DISMISSED_KEY, 'true');
};
return (
<Stack gap="lg">
{!hideTitle && (
<div>
<Text fw={600} size="lg">
{t("settings.general.title", "General")}
</Text>
<Text fw={600} size="lg">{t('settings.general.title', 'General')}</Text>
<Text size="sm" c="dimmed">
{t("settings.general.description", "Configure general application preferences.")}
{t('settings.general.description', 'Configure general application preferences.')}
</Text>
</div>
)}
{loginDisabled && !bannerDismissed && (
<Paper withBorder p="md" radius="md" style={{ background: "var(--mantine-color-blue-0)", position: "relative" }}>
<Paper withBorder p="md" radius="md" style={{ background: 'var(--mantine-color-blue-0)', position: 'relative' }}>
<ActionIcon
variant="subtle"
color="gray"
size="sm"
style={{ position: "absolute", top: "0.5rem", right: "0.5rem" }}
style={{ position: 'absolute', top: '0.5rem', right: '0.5rem' }}
onClick={handleDismissBanner}
aria-label={t("settings.general.enableFeatures.dismiss", "Dismiss")}
aria-label={t('settings.general.enableFeatures.dismiss', 'Dismiss')}
>
<LocalIcon icon="close-rounded" width="1rem" height="1rem" />
</ActionIcon>
<Stack gap="sm">
<Group gap="xs">
<LocalIcon
icon="admin-panel-settings-rounded"
width="1.2rem"
height="1.2rem"
style={{ color: "var(--mantine-color-blue-6)" }}
/>
<Text fw={600} size="sm" style={{ color: "var(--mantine-color-blue-9)" }}>
{t("settings.general.enableFeatures.title", "For System Administrators")}
<LocalIcon icon="admin-panel-settings-rounded" width="1.2rem" height="1.2rem" style={{ color: 'var(--mantine-color-blue-6)' }} />
<Text fw={600} size="sm" style={{ color: 'var(--mantine-color-blue-9)' }}>
{t('settings.general.enableFeatures.title', 'For System Administrators')}
</Text>
</Group>
<Text size="sm" c="dimmed">
{t(
"settings.general.enableFeatures.intro",
"Enable user authentication, team management, and workspace features for your organization.",
)}
{t('settings.general.enableFeatures.intro', 'Enable user authentication, team management, and workspace features for your organization.')}
</Text>
<Group gap="xs" wrap="wrap">
<Text size="sm" c="dimmed">
{t("settings.general.enableFeatures.action", "Configure")}
{t('settings.general.enableFeatures.action', 'Configure')}
</Text>
<Code>SECURITY_ENABLELOGIN=true</Code>
<Text size="sm" c="dimmed">
{t("settings.general.enableFeatures.and", "and")}
{t('settings.general.enableFeatures.and', 'and')}
</Text>
<Code>DISABLE_ADDITIONAL_FEATURES=false</Code>
</Group>
<Text size="xs" c="dimmed" fs="italic">
{t(
"settings.general.enableFeatures.benefit",
"Enables user roles, team collaboration, admin controls, and enterprise features.",
)}
{t('settings.general.enableFeatures.benefit', 'Enables user roles, team collaboration, admin controls, and enterprise features.')}
</Text>
<Anchor
href="https://docs.stirlingpdf.com/Configuration/System%20and%20Security/"
href="https://docs.stirlingpdf.com/Advanced%20Configuration/System%20and%20Security"
target="_blank"
size="sm"
style={{ color: "var(--mantine-color-blue-6)" }}
style={{ color: 'var(--mantine-color-blue-6)' }}
>
{t("settings.general.enableFeatures.learnMore", "Learn more in documentation")} →
{t('settings.general.enableFeatures.learnMore', 'Learn more in documentation')} →
</Anchor>
</Stack>
</Paper>
@@ -222,52 +142,36 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({ hideTitle = false }) =>
<Group justify="space-between" align="center">
<div>
<Text fw={600} size="sm">
{t("settings.general.updates.title", "Software Updates")}
{t('settings.general.updates.title', 'Software Updates')}
</Text>
<Text size="xs" c="dimmed" mt={4}>
{t("settings.general.updates.description", "Check for updates and view version information")}
{t('settings.general.updates.description', 'Check for updates and view version information')}
</Text>
</div>
{updateSummary && (
<Badge color={updateSummary.max_priority === "urgent" ? "red" : "blue"} variant="filled">
{updateSummary.max_priority === "urgent"
? t("update.urgentUpdateAvailable", "Urgent Update")
: t("update.updateAvailable", "Update Available")}
<Badge
color={updateSummary.max_priority === 'urgent' ? 'red' : 'blue'}
variant="filled"
>
{updateSummary.max_priority === 'urgent'
? t('update.urgentUpdateAvailable', 'Urgent Update')
: t('update.updateAvailable', 'Update Available')}
</Badge>
)}
</Group>
</div>
{isTauriApp && (
<Group justify="space-between" align="center">
<div>
<Text size="sm" c="dimmed">
{t("settings.general.updates.currentFrontendVersion", "Current Frontend Version")}:{" "}
<Text component="span" fw={500}>
{frontendVersionLabel}
</Text>
</Text>
{mismatchVersion && (
<Text size="sm" c="red" mt={4}>
{t(
"settings.general.updates.versionMismatch",
"Warning: A mismatch has been detected between the client version and the AppConfig version. Using different versions can lead to compatibility issues, errors, and security risks. Please ensure that server and client are using the same version.",
)}
</Text>
)}
</div>
</Group>
)}
<Group justify="space-between" align="center">
<div>
<Text size="sm" c="dimmed">
{t("settings.general.updates.currentBackendVersion", "Current Backend Version")}:{" "}
{t('settings.general.updates.currentVersion', 'Current Version')}:{' '}
<Text component="span" fw={500}>
{config.appVersion}
</Text>
</Text>
{updateSummary && (
<Text size="sm" c="dimmed" mt={4}>
{t("settings.general.updates.latestVersion", "Latest Version")}:{" "}
{t('settings.general.updates.latestVersion', 'Latest Version')}:{' '}
<Text component="span" fw={500} c="blue">
{updateSummary.latest_version}
</Text>
@@ -282,16 +186,16 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({ hideTitle = false }) =>
loading={checkingUpdate}
leftSection={<LocalIcon icon="refresh-rounded" width="1rem" height="1rem" />}
>
{t("settings.general.updates.checkForUpdates", "Check for Updates")}
{t('settings.general.updates.checkForUpdates', 'Check for Updates')}
</Button>
{updateSummary && (
<Button
size="sm"
color={updateSummary.max_priority === "urgent" ? "red" : "blue"}
color={updateSummary.max_priority === 'urgent' ? 'red' : 'blue'}
onClick={() => setUpdateModalOpened(true)}
leftSection={<LocalIcon icon="system-update-alt-rounded" width="1rem" height="1rem" />}
>
{t("settings.general.updates.viewDetails", "View Details")}
{t('settings.general.updates.viewDetails', 'View Details')}
</Button>
)}
</Group>
@@ -300,15 +204,15 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({ hideTitle = false }) =>
{updateSummary?.any_breaking && (
<Alert
color="orange"
title={t("update.breakingChangesDetected", "Breaking Changes Detected")}
title={t('update.breakingChangesDetected', 'Breaking Changes Detected')}
styles={{
title: { fontWeight: 600 },
title: { fontWeight: 600 }
}}
>
<Text size="sm">
{t(
"update.breakingChangesMessage",
"Some versions contain breaking changes. Please review the migration guides before updating.",
'update.breakingChangesMessage',
'Some versions contain breaking changes. Please review the migration guides before updating.'
)}
</Text>
</Alert>
@@ -319,102 +223,87 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({ hideTitle = false }) =>
<Paper withBorder p="md" radius="md">
<Stack gap="md">
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div>
<Text fw={500} size="sm">
{t("settings.general.defaultToolPickerMode", "Default tool picker mode")}
{t('settings.general.defaultToolPickerMode', 'Default tool picker mode')}
</Text>
<Text size="xs" c="dimmed" mt={4}>
{t(
"settings.general.defaultToolPickerModeDescription",
"Choose whether the tool picker opens in fullscreen or sidebar by default",
)}
{t('settings.general.defaultToolPickerModeDescription', 'Choose whether the tool picker opens in fullscreen or sidebar by default')}
</Text>
</div>
<SegmentedControl
value={preferences.defaultToolPanelMode}
onChange={(val: string) => updatePreference("defaultToolPanelMode", val as ToolPanelMode)}
onChange={(val: string) => updatePreference('defaultToolPanelMode', val as ToolPanelMode)}
data={[
{ label: t("settings.general.mode.sidebar", "Sidebar"), value: "sidebar" },
{ label: t("settings.general.mode.fullscreen", "Fullscreen"), value: "fullscreen" },
{ label: t('settings.general.mode.sidebar', 'Sidebar'), value: 'sidebar' },
{ label: t('settings.general.mode.fullscreen', 'Fullscreen'), value: 'fullscreen' },
]}
/>
</div>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div>
<Text fw={500} size="sm">
{t("settings.general.hideUnavailableTools", "Hide unavailable tools")}
{t('settings.general.hideUnavailableTools', 'Hide unavailable tools')}
</Text>
<Text size="xs" c="dimmed" mt={4}>
{t(
"settings.general.hideUnavailableToolsDescription",
"Remove tools that have been disabled by your server instead of showing them greyed out.",
)}
{t('settings.general.hideUnavailableToolsDescription', 'Remove tools that have been disabled by your server instead of showing them greyed out.')}
</Text>
</div>
<Switch
checked={preferences.hideUnavailableTools}
onChange={(event) => updatePreference("hideUnavailableTools", event.currentTarget.checked)}
onChange={(event) => updatePreference('hideUnavailableTools', event.currentTarget.checked)}
/>
</div>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div>
<Text fw={500} size="sm">
{t("settings.general.hideUnavailableConversions", "Hide unavailable conversions")}
{t('settings.general.hideUnavailableConversions', 'Hide unavailable conversions')}
</Text>
<Text size="xs" c="dimmed" mt={4}>
{t(
"settings.general.hideUnavailableConversionsDescription",
"Remove disabled conversion options in the Convert tool instead of showing them greyed out.",
)}
{t('settings.general.hideUnavailableConversionsDescription', 'Remove disabled conversion options in the Convert tool instead of showing them greyed out.')}
</Text>
</div>
<Switch
checked={preferences.hideUnavailableConversions}
onChange={(event) => updatePreference("hideUnavailableConversions", event.currentTarget.checked)}
onChange={(event) => updatePreference('hideUnavailableConversions', event.currentTarget.checked)}
/>
</div>
<Tooltip
label={t(
"settings.general.autoUnzipTooltip",
"Automatically extract ZIP files returned from API operations. Disable to keep ZIP files intact. This does not affect automation workflows.",
)}
label={t('settings.general.autoUnzipTooltip', 'Automatically extract ZIP files returned from API operations. Disable to keep ZIP files intact. This does not affect automation workflows.')}
multiline
w={300}
withArrow
>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", cursor: "help" }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', cursor: 'help' }}>
<div>
<Text fw={500} size="sm">
{t("settings.general.autoUnzip", "Auto-unzip API responses")}
{t('settings.general.autoUnzip', 'Auto-unzip API responses')}
</Text>
<Text size="xs" c="dimmed" mt={4}>
{t("settings.general.autoUnzipDescription", "Automatically extract files from ZIP responses")}
{t('settings.general.autoUnzipDescription', 'Automatically extract files from ZIP responses')}
</Text>
</div>
<Switch
checked={preferences.autoUnzip}
onChange={(event) => updatePreference("autoUnzip", event.currentTarget.checked)}
onChange={(event) => updatePreference('autoUnzip', event.currentTarget.checked)}
/>
</div>
</Tooltip>
<Tooltip
label={t(
"settings.general.autoUnzipFileLimitTooltip",
"Only unzip if the ZIP contains this many files or fewer. Set higher to extract larger ZIPs.",
)}
label={t('settings.general.autoUnzipFileLimitTooltip', 'Only unzip if the ZIP contains this many files or fewer. Set higher to extract larger ZIPs.')}
multiline
w={300}
withArrow
>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", cursor: "help" }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', cursor: 'help' }}>
<div>
<Text fw={500} size="sm">
{t("settings.general.autoUnzipFileLimit", "Auto-unzip file limit")}
{t('settings.general.autoUnzipFileLimit', 'Auto-unzip file limit')}
</Text>
<Text size="xs" c="dimmed" mt={4}>
{t("settings.general.autoUnzipFileLimitDescription", "Maximum number of files to extract from ZIP")}
{t('settings.general.autoUnzipFileLimitDescription', 'Maximum number of files to extract from ZIP')}
</Text>
</div>
<NumberInput
@@ -422,12 +311,9 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({ hideTitle = false }) =>
onChange={setFileLimitInput}
onBlur={() => {
const numValue = Number(fileLimitInput);
const finalValue =
!fileLimitInput || isNaN(numValue) || numValue < 1 || numValue > 100
? DEFAULT_AUTO_UNZIP_FILE_LIMIT
: numValue;
const finalValue = (!fileLimitInput || isNaN(numValue) || numValue < 1 || numValue > 100) ? DEFAULT_AUTO_UNZIP_FILE_LIMIT : numValue;
setFileLimitInput(finalValue);
updatePreference("autoUnzipFileLimit", finalValue);
updatePreference('autoUnzipFileLimit', finalValue);
}}
min={1}
max={100}
@@ -450,7 +336,7 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({ hideTitle = false }) =>
machineInfo={{
machineType: config.machineType,
activeSecurity: config.activeSecurity ?? false,
licenseType: config.license ?? "NORMAL",
licenseType: config.license ?? 'NORMAL',
}}
/>
)}
@@ -99,7 +99,7 @@ const StampPositionFormattingSettings = ({ parameters, onParameterChange, disabl
<Group className={styles.sliderGroup} align="center">
<NumberInput
value={parameters.fontSize}
onChange={(v) => onParameterChange('fontSize', typeof v === 'number' && v > 0 ? v : 1)}
onChange={(v) => onParameterChange('fontSize', typeof v === 'number' ? v : 1)}
min={1}
max={400}
step={1}
@@ -114,7 +114,6 @@ const StampPositionFormattingSettings = ({ parameters, onParameterChange, disabl
max={400}
step={1}
className={styles.slider}
disabled={disabled}
/>
</Group>
</Stack>
@@ -1,169 +1,18 @@
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { Stack, Textarea, TextInput, Select, Button, Text, Divider, Accordion, Code, Group, Badge, Box, Paper } from "@mantine/core";
import { Stack, Textarea, TextInput, Select, Button, Text, Divider } from "@mantine/core";
import { AddStampParameters } from "@app/components/tools/addStamp/useAddStampParameters";
import ButtonSelector from "@app/components/shared/ButtonSelector";
import styles from "@app/components/tools/addStamp/StampPreview.module.css";
import { getDefaultFontSizeForAlphabet } from "@app/components/tools/addStamp/StampPreviewUtils";
import { Z_INDEX_AUTOMATE_DROPDOWN } from "@app/styles/zIndex";
const STAMP_TEMPLATES = [
{
id: 'page-numbers',
name: 'Page Numbers',
text: 'Page @page_number of @total_pages',
position: 2, // bottom center
},
{
id: 'draft',
name: 'Draft Watermark',
text: 'DRAFT - @date',
position: 5, // center
},
{
id: 'doc-info',
name: 'Document Info',
text: '@filename\nCreated: @date{dd MMM yyyy}',
position: 7, // top left
},
{
id: 'legal-footer',
name: 'Legal Footer',
text: '© @year - All Rights Reserved\n@filename - Page @page_number',
position: 2, // bottom center
},
{
id: 'european-date',
name: 'European Date (DD/MM/YYYY)',
text: '@date{dd/MM/yyyy}',
position: 9, // top right
},
{
id: 'timestamp',
name: 'Timestamp',
text: '@date{dd/MM/yyyy HH:mm}',
position: 9, // top right
},
];
const resolveVariablesForPreview = (text: string, filename?: string): string => {
const now = new Date();
const pad = (n: number) => String(n).padStart(2, '0');
const ESCAPED_AT_PLACEHOLDER = '\uE000ESCAPED_AT\uE000';
let result = text.replace(/@@/g, ESCAPED_AT_PLACEHOLDER);
const actualFilename = filename || 'sample-document.pdf';
const filenameWithoutExt = actualFilename.includes('.')
? actualFilename.substring(0, actualFilename.lastIndexOf('.'))
: actualFilename;
const sampleData: Record<string, string> = {
'@datetime': `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`,
'@date': `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`,
'@time': `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`,
'@year': String(now.getFullYear()),
'@month': pad(now.getMonth() + 1),
'@day': pad(now.getDate()),
// Page info - cannot be previewed, show placeholder
'@page_number': '?',
'@page': '?',
'@total_pages': '?',
'@page_count': '?',
// Filename - use actual file if provided
'@filename': filenameWithoutExt,
'@filename_full': actualFilename,
// Metadata - cannot be read from PDF in frontend, show placeholder
'@author': '?',
'@title': '?',
'@subject': '?',
// UUID - will be random each time
'@uuid': '????????',
};
result = result.replace(/@date\{([^}]+)\}/g, (match, format) => {
try {
return format
.replace(/yyyy/g, String(now.getFullYear()))
.replace(/yy/g, String(now.getFullYear()).slice(-2))
.replace(/MMMM/g, now.toLocaleString('default', { month: 'long' }))
.replace(/MMM/g, now.toLocaleString('default', { month: 'short' }))
.replace(/MM/g, pad(now.getMonth() + 1))
.replace(/dd/g, pad(now.getDate()))
.replace(/HH/g, pad(now.getHours()))
.replace(/hh/g, pad(now.getHours() % 12 || 12))
.replace(/mm/g, pad(now.getMinutes()))
.replace(/ss/g, pad(now.getSeconds()));
} catch {
return match;
}
});
Object.entries(sampleData).forEach(([key, value]) => {
result = result.split(key).join(value);
});
result = result.replace(new RegExp(ESCAPED_AT_PLACEHOLDER, 'g'), '@');
result = result.replace(/\\n/g, '\n');
return result;
};
interface ClickableCodeProps {
children: React.ReactNode;
onClick: () => void;
block?: boolean;
}
const ClickableCode = ({ children, onClick, block = false }: ClickableCodeProps) => (
<Code
tabIndex={0}
role="button"
style={{
cursor: 'pointer',
display: block ? 'block' : undefined,
}}
onClick={onClick}
onKeyDown={(e: React.KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onClick();
}
}}
>
{children}
</Code>
);
const StampTextPreview = ({ stampText, filename }: { stampText: string; filename?: string }) => {
const { t } = useTranslation();
const resolvedText = useMemo(() => {
if (!stampText.trim()) return '';
return resolveVariablesForPreview(stampText, filename);
}, [stampText, filename]);
if (!stampText.trim()) return null;
return (
<Paper p="xs" withBorder bg="var(--mantine-color-default)">
<Text size="xs" c="dimmed" mb={4}>{t('AddStampRequest.preview', 'Preview:')}</Text>
<Text size="sm" style={{ whiteSpace: 'pre-wrap', fontFamily: 'monospace', wordBreak: 'break-word' }}>
{resolvedText}
</Text>
</Paper>
);
};
interface StampSetupSettingsProps {
parameters: AddStampParameters;
onParameterChange: <K extends keyof AddStampParameters>(key: K, value: AddStampParameters[K]) => void;
disabled?: boolean;
filename?: string;
}
const StampSetupSettings = ({ parameters, onParameterChange, disabled = false, filename }: StampSetupSettingsProps) => {
const StampSetupSettings = ({ parameters, onParameterChange, disabled = false }: StampSetupSettingsProps) => {
const { t } = useTranslation();
return (
@@ -192,172 +41,14 @@ const StampSetupSettings = ({ parameters, onParameterChange, disabled = false, f
{parameters.stampType === 'text' && (
<>
{/* Template Selector - always shows placeholder, doesn't persist selection */}
<Select
label={t('AddStampRequest.useTemplate', 'Use Template')}
placeholder={t('AddStampRequest.selectTemplate', 'Select a template...')}
value={null}
data={STAMP_TEMPLATES.map(template => ({
value: template.id,
label: t(`AddStampRequest.template.${template.id}`, template.name)
}))}
onChange={(value) => {
const template = STAMP_TEMPLATES.find(t => t.id === value);
if (template) {
onParameterChange('stampText', template.text);
onParameterChange('position', template.position as any);
}
}}
clearable
disabled={disabled}
comboboxProps={{ withinPortal: true, zIndex: Z_INDEX_AUTOMATE_DROPDOWN }}
/>
<Textarea
label={t('AddStampRequest.stampText', 'Stamp Text')}
description={t('AddStampRequest.stampTextDescription', 'Use dynamic variables below. Use @@ for literal @. Use \\n for new lines.')}
value={parameters.stampText}
onChange={(e) => onParameterChange('stampText', e.currentTarget.value)}
autosize
minRows={2}
disabled={disabled}
/>
{/* Live Preview */}
<StampTextPreview stampText={parameters.stampText} filename={filename} />
<Accordion variant="contained" radius="sm">
<Accordion.Item value="variables">
<Accordion.Control>
<Group gap="xs">
<Text size="sm" fw={500}>{t('AddStampRequest.dynamicVariables', 'Dynamic Variables')}</Text>
<Badge size="xs" variant="light" color="blue">{t('AddStampRequest.clickToExpand', 'Click to expand')}</Badge>
</Group>
</Accordion.Control>
<Accordion.Panel>
<Stack gap="xs">
<Text size="xs" c="dimmed" mb="xs">
{t('AddStampRequest.variablesHelp', 'Click on any variable to insert it into your stamp text. Use @@ for literal @.')}
</Text>
<Box>
<Text size="xs" fw={600} mb={4}>{t('AddStampRequest.dateTimeVars', 'Date & Time')}</Text>
<Group gap="xs">
<ClickableCode onClick={() => onParameterChange('stampText', parameters.stampText + '@date')}>
@date
</ClickableCode>
<Text size="xs" c="dimmed">— {t('AddStampRequest.dateDesc', 'Current date')} (YYYY-MM-DD)</Text>
</Group>
<Group gap="xs" mt={4}>
<ClickableCode onClick={() => onParameterChange('stampText', parameters.stampText + '@time')}>
@time
</ClickableCode>
<Text size="xs" c="dimmed">— {t('AddStampRequest.timeDesc', 'Current time')} (HH:mm:ss)</Text>
</Group>
<Group gap="xs" mt={4}>
<ClickableCode onClick={() => onParameterChange('stampText', parameters.stampText + '@datetime')}>
@datetime
</ClickableCode>
<Text size="xs" c="dimmed">— {t('AddStampRequest.datetimeDesc', 'Date and time combined')}</Text>
</Group>
<Group gap="xs" mt={4}>
<ClickableCode onClick={() => onParameterChange('stampText', parameters.stampText + '@date{dd/MM/yyyy}')}>
@date&#123;format&#125;
</ClickableCode>
<Text size="xs" c="dimmed">— {t('AddStampRequest.customDateDesc', 'Custom format')}</Text>
</Group>
<Group gap="xs" mt={4}>
<ClickableCode onClick={() => onParameterChange('stampText', parameters.stampText + '@year')}>
@year
</ClickableCode>
<ClickableCode onClick={() => onParameterChange('stampText', parameters.stampText + '@month')}>
@month
</ClickableCode>
<ClickableCode onClick={() => onParameterChange('stampText', parameters.stampText + '@day')}>
@day
</ClickableCode>
<Text size="xs" c="dimmed">— {t('AddStampRequest.yearMonthDayDesc', 'Individual date parts')}</Text>
</Group>
</Box>
<Divider my="xs" />
<Box>
<Text size="xs" fw={600} mb={4}>{t('AddStampRequest.pageVars', 'Page Information')}</Text>
<Group gap="xs">
<ClickableCode onClick={() => onParameterChange('stampText', parameters.stampText + '@page_number')}>
@page_number
</ClickableCode>
<Text size="xs" c="dimmed">— {t('AddStampRequest.pageNumberDesc', 'Current page number')}</Text>
</Group>
<Group gap="xs" mt={4}>
<ClickableCode onClick={() => onParameterChange('stampText', parameters.stampText + '@total_pages')}>
@total_pages
</ClickableCode>
<Text size="xs" c="dimmed">— {t('AddStampRequest.totalPagesDesc', 'Total number of pages')}</Text>
</Group>
</Box>
<Divider my="xs" />
<Box>
<Text size="xs" fw={600} mb={4}>{t('AddStampRequest.fileVars', 'File Information')}</Text>
<Group gap="xs">
<ClickableCode onClick={() => onParameterChange('stampText', parameters.stampText + '@filename')}>
@filename
</ClickableCode>
<Text size="xs" c="dimmed">— {t('AddStampRequest.filenameDesc', 'Filename without extension')}</Text>
</Group>
<Group gap="xs" mt={4}>
<ClickableCode onClick={() => onParameterChange('stampText', parameters.stampText + '@filename_full')}>
@filename_full
</ClickableCode>
<Text size="xs" c="dimmed">— {t('AddStampRequest.filenameFullDesc', 'Filename with extension')}</Text>
</Group>
</Box>
<Divider my="xs" />
<Box>
<Text size="xs" fw={600} mb={4}>{t('AddStampRequest.metadataVars', 'Document Metadata')}</Text>
<Group gap="xs">
<ClickableCode onClick={() => onParameterChange('stampText', parameters.stampText + '@author')}>
@author
</ClickableCode>
<ClickableCode onClick={() => onParameterChange('stampText', parameters.stampText + '@title')}>
@title
</ClickableCode>
<ClickableCode onClick={() => onParameterChange('stampText', parameters.stampText + '@subject')}>
@subject
</ClickableCode>
</Group>
<Text size="xs" c="dimmed" mt={4}>— {t('AddStampRequest.metadataDesc', 'From PDF document properties')}</Text>
</Box>
<Divider my="xs" />
<Box>
<Text size="xs" fw={600} mb={4}>{t('AddStampRequest.otherVars', 'Other')}</Text>
<Group gap="xs">
<ClickableCode onClick={() => onParameterChange('stampText', parameters.stampText + '@uuid')}>
@uuid
</ClickableCode>
<Text size="xs" c="dimmed">— {t('AddStampRequest.uuidDesc', 'Short unique identifier (8 chars)')}</Text>
</Group>
</Box>
<Divider my="xs" />
<Box>
<Text size="xs" fw={600} mb={4}>{t('AddStampRequest.examples', 'Examples')}</Text>
<Stack gap={4}>
<ClickableCode block onClick={() => onParameterChange('stampText', 'Page @page_number of @total_pages')}>
Page @page_number of @total_pages
</ClickableCode>
<ClickableCode block onClick={() => onParameterChange('stampText', 'Created: @date{dd/MM/yyyy HH:mm}')}>
Created: @date&#123;dd/MM/yyyy HH:mm&#125;
</ClickableCode>
<ClickableCode block onClick={() => onParameterChange('stampText', '© @year @author')}>
© @year @author
</ClickableCode>
<ClickableCode block onClick={() => onParameterChange('stampText', '@filename\\n@date')}>
@filename\n@date ({t('AddStampRequest.multiLine', 'multi-line')})
</ClickableCode>
</Stack>
</Box>
</Stack>
</Accordion.Panel>
</Accordion.Item>
</Accordion>
<Select
label={t('AddStampRequest.alphabet', 'Alphabet')}
value={parameters.alphabet}
@@ -22,7 +22,7 @@ export const defaultParameters: AddStampParameters = {
stampType: 'text',
stampText: '',
alphabet: 'roman',
fontSize: 40,
fontSize: 80,
rotation: 0,
opacity: 50,
position: 5,
@@ -1,7 +1,7 @@
import React, { createContext, useContext, useState, useCallback, ReactNode, useMemo, useRef, useEffect } from 'react';
import { FileId } from '@app/types/file';
import { useFileActions, useFileState } from '@app/contexts/FileContext';
import { PDFDocument, PDFPage } from '@app/types/pageEditor';
import { PDFPage } from '@app/types/pageEditor';
import { MAX_PAGE_EDITOR_FILES } from '@app/components/pageEditor/fileColors';
// PageEditorFile is now defined locally in consuming components
@@ -129,10 +129,6 @@ interface PageEditorContextValue {
// Update file order based on page positions (when pages are manually reordered)
updateFileOrderFromPages: (pages: PDFPage[]) => void;
persistedDocument: PDFDocument | null;
persistedDocumentSignature: string | null;
savePersistedDocument: (document: PDFDocument, signature: string) => void;
clearPersistedDocument: () => void;
}
const PageEditorContext = createContext<PageEditorContextValue | undefined>(undefined);
@@ -145,19 +141,6 @@ export function PageEditorProvider({ children }: PageEditorProviderProps) {
const [currentPages, setCurrentPages] = useState<PDFPage[] | null>(null);
const [reorderedPages, setReorderedPages] = useState<PDFPage[] | null>(null);
const [persistedDocument, setPersistedDocument] = useState<PDFDocument | null>(null);
const [persistedDocumentSignature, setPersistedDocumentSignature] = useState<string | null>(null);
const savePersistedDocument = useCallback((document: PDFDocument, signature: string) => {
setPersistedDocument(document);
setPersistedDocumentSignature(signature);
}, []);
const clearPersistedDocument = useCallback(() => {
setPersistedDocument(null);
setPersistedDocumentSignature(null);
}, []);
// Page editor's own file order (independent of FileContext)
const [fileOrder, setFileOrder] = useState<FileId[]>([]);
@@ -165,20 +148,6 @@ export function PageEditorProvider({ children }: PageEditorProviderProps) {
const { actions: fileActions } = useFileActions();
const { state } = useFileState();
const fileContextSignature = useMemo(() => {
return state.files.ids
.map(id => `${id}:${state.files.byId[id]?.versionNumber ?? 0}`)
.join(',');
}, [state.files.ids, state.files.byId]);
const prevFileContextSignature = useRef<string | null>(null);
useEffect(() => {
if (prevFileContextSignature.current !== fileContextSignature) {
prevFileContextSignature.current = fileContextSignature;
clearPersistedDocument();
}
}, [fileContextSignature, clearPersistedDocument]);
// Keep a ref to always read latest state in stable callbacks
const stateRef = useRef(state);
useEffect(() => {
@@ -234,7 +203,7 @@ export function PageEditorProvider({ children }: PageEditorProviderProps) {
}
});
}, 100);
}, [state.files.ids, state.files.byId, fileActions]);
}, [state.files.ids, state.files.byId, fileActions]);
const updateCurrentPages = useCallback((pages: PDFPage[] | null) => {
setCurrentPages(pages);
@@ -360,10 +329,6 @@ export function PageEditorProvider({ children }: PageEditorProviderProps) {
deselectAll,
reorderFiles,
updateFileOrderFromPages,
persistedDocument,
persistedDocumentSignature,
savePersistedDocument,
clearPersistedDocument,
}), [
currentPages,
updateCurrentPages,
@@ -376,10 +341,6 @@ export function PageEditorProvider({ children }: PageEditorProviderProps) {
deselectAll,
reorderFiles,
updateFileOrderFromPages,
persistedDocument,
persistedDocumentSignature,
savePersistedDocument,
clearPersistedDocument,
]);
return (
@@ -180,7 +180,6 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
subcategoryId: SubcategoryId.GENERAL,
maxFiles: -1,
endpoints: ["multi-tool"],
synonyms: getSynonyms(t, "multiTool"),
supportsAutomate: false,
automationSettings: null
@@ -219,7 +218,6 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
description: t("home.sign.desc", "Adds signature to PDF by drawing, text or image"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.SIGNING,
endpoints: ["sign"],
operationConfig: signOperationConfig,
automationSettings: SignSettings, // TODO:: not all settings shown, suggested next tools shown
synonyms: getSynonyms(t, "sign"),
@@ -232,7 +230,6 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
description: t('home.addText.desc', 'Add custom text anywhere in your PDF'),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.GENERAL,
endpoints: ["sign"],
operationConfig: signOperationConfig,
automationSettings: null,
synonyms: getSynonyms(t, 'addText'),
@@ -245,7 +242,6 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
description: t('home.addImage.desc', 'Add images anywhere in your PDF'),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.GENERAL,
endpoints: ["add-image"],
operationConfig: signOperationConfig,
automationSettings: null,
synonyms: getSynonyms(t, 'addImage'),
@@ -259,7 +255,6 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.GENERAL,
workbench: 'viewer',
endpoints: ["view-pdf"],
operationConfig: signOperationConfig,
automationSettings: null,
synonyms: getSynonyms(t, 'annotate'),
@@ -398,7 +393,6 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.DOCUMENT_REVIEW,
endpoints: ["view-pdf"],
synonyms: getSynonyms(t, "read"),
supportsAutomate: false,
automationSettings: null
@@ -716,8 +710,6 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
description: t("home.adjustContrast.desc", "Adjust colors and contrast of PDF documents"),
categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
maxFiles: -1,
endpoints: ["adjust-contrast"],
operationConfig: adjustContrastOperationConfig,
automationSettings: AdjustContrastSingleStepSettings,
synonyms: getSynonyms(t, "adjustContrast"),
@@ -819,7 +811,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
description: t("home.devFolderScanning.desc", "Link to automated folder scanning guide"),
categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.DEVELOPER_TOOLS,
link: "https://docs.stirlingpdf.com/Configuration/Folder%20Scanning/",
link: "https://docs.stirlingpdf.com/Advanced%20Configuration/Folder%20Scanning/",
synonyms: getSynonyms(t, "devFolderScanning"),
supportsAutomate: false,
automationSettings: null
@@ -831,7 +823,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
description: t("home.devSsoGuide.desc", "Link to SSO guide"),
categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.DEVELOPER_TOOLS,
link: "https://docs.stirlingpdf.com/Configuration/Single%20Sign-On%20Configuration/",
link: "https://docs.stirlingpdf.com/Advanced%20Configuration/Single%20Sign-On%20Configuration",
synonyms: getSynonyms(t, "devSsoGuide"),
supportsAutomate: false,
automationSettings: null
@@ -858,7 +850,6 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
subcategoryId: SubcategoryId.GENERAL,
maxFiles: 2,
endpoints: ["compare"],
operationConfig: undefined,
automationSettings: null,
synonyms: getSynonyms(t, "compare"),
@@ -20,13 +20,10 @@ let batchTimer: number | null = null;
// Track active thumbnail requests to prevent duplicates across components
const activeRequests = new Map<string, Promise<string | null>>();
// Cache ArrayBuffers to avoid reading the same file multiple times
const fileArrayBufferCache = new Map<File, ArrayBuffer>();
// Batch processing configuration
const BATCH_SIZE = 10; // Process thumbnails in batches of 10 for faster initial load
const BATCH_DELAY = 50; // Wait 50ms to collect requests before processing
const PRIORITY_BATCH_DELAY = 10; // Very fast processing for the first batch (visible pages)
const BATCH_SIZE = 20; // Process thumbnails in batches of 20 for better UI responsiveness
const BATCH_DELAY = 100; // Wait 100ms to collect requests before processing
const PRIORITY_BATCH_DELAY = 50; // Faster processing for the first batch (visible pages)
// Process the queue in batches for better performance
async function processRequestQueue() {
@@ -71,13 +68,8 @@ async function processRequestQueue() {
try {
const pageNumbers = requests.map(req => req.pageNumber);
const arrayBuffer = await file.arrayBuffer();
// Get or create cached ArrayBuffer to avoid reading file multiple times
let arrayBuffer = fileArrayBufferCache.get(file);
if (!arrayBuffer) {
arrayBuffer = await file.arrayBuffer();
fileArrayBufferCache.set(file, arrayBuffer);
}
// Use quickKey for PDF document caching (same metadata, consistent format)
const fileId = createQuickKey(file) as FileId;
@@ -114,10 +106,6 @@ async function processRequestQueue() {
}
} finally {
isProcessingQueue = false;
// Clean up ArrayBuffer cache when queue is empty
if (requestQueue.length === 0) {
fileArrayBufferCache.clear();
}
}
}
@@ -175,9 +163,6 @@ export function useThumbnailGeneration() {
activeRequests.clear();
isProcessingQueue = false;
// Clear ArrayBuffer cache
fileArrayBufferCache.clear();
thumbnailGenerationService.destroy();
}, []);
@@ -265,13 +265,17 @@ export class EnhancedPDFProcessingService {
this.notifyListeners();
}
// Create placeholder pages for remaining pages without touching PDF.js
// Create placeholder pages for remaining pages
for (let i = priorityCount + 1; i <= totalPages; i++) {
// Load page just to get rotation
const page = await pdf.getPage(i);
const rotation = page.rotate || 0;
pages.push({
id: `${createQuickKey(file)}-page-${i}`,
pageNumber: i,
thumbnail: null, // Will be loaded lazily
rotation: 0,
rotation,
selected: false
});
}
@@ -333,13 +337,17 @@ export class EnhancedPDFProcessingService {
}
}
// Create placeholders for remaining pages without invoking PDF.js
// Create placeholders for remaining pages
for (let i = firstChunkEnd + 1; i <= totalPages; i++) {
// Load page just to get rotation
const page = await pdf.getPage(i);
const rotation = page.rotate || 0;
pages.push({
id: `${createQuickKey(file)}-page-${i}`,
pageNumber: i,
thumbnail: null,
rotation: 0,
rotation,
selected: false
});
}
@@ -369,11 +377,15 @@ export class EnhancedPDFProcessingService {
// Create placeholder pages without thumbnails
const pages: PDFPage[] = [];
for (let i = 1; i <= totalPages; i++) {
// Load page just to get rotation
const page = await pdf.getPage(i);
const rotation = page.rotate || 0;
pages.push({
id: `${createQuickKey(file)}-page-${i}`,
pageNumber: i,
thumbnail: null,
rotation: 0,
rotation,
selected: false
});
}
+1 -1
View File
@@ -56,7 +56,7 @@ export class FileAnalyzer {
/**
* Quick PDF analysis without full processing
*/
static async quickPDFAnalysis(file: File): Promise<{
private static async quickPDFAnalysis(file: File): Promise<{
pageCount: number;
isEncrypted: boolean;
isCorrupted: boolean;
@@ -38,7 +38,7 @@ const FREE_LICENSE_INFO: LicenseInfo = {
const BASE_NO_LOGIN_CONFIG: AppConfig = {
enableAnalytics: true,
appVersion: '2.4.3',
appVersion: '2.4.1',
serverCertificateEnabled: false,
enableAlphaFunctionality: false,
serverPort: 8080,
-1
View File
@@ -80,7 +80,6 @@ const AddStamp = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
parameters={params.parameters}
onParameterChange={params.updateParameter}
disabled={endpointLoading}
filename={selectedFiles[0]?.name}
/>
),
});
+9 -18
View File
@@ -1,5 +1,4 @@
import { useTranslation } from "react-i18next";
import { useMemo } from "react";
import { createToolFlow } from "@app/components/tools/shared/createToolFlow";
import CardSelector from "@app/components/shared/CardSelector";
import SplitSettings from "@app/components/tools/split/SplitSettings";
@@ -9,8 +8,7 @@ import { useBaseTool } from "@app/hooks/tools/shared/useBaseTool";
import { useSplitMethodTips } from "@app/components/tooltips/useSplitMethodTips";
import { useSplitSettingsTips } from "@app/components/tooltips/useSplitSettingsTips";
import { BaseToolProps, ToolComponent } from "@app/types/tool";
import { type SplitMethod, METHOD_OPTIONS, type MethodOption, ENDPOINTS } from "@app/constants/splitConstants";
import { useMultipleEndpointsEnabled } from "@app/hooks/useEndpointConfig";
import { type SplitMethod, METHOD_OPTIONS, type MethodOption } from "@app/constants/splitConstants";
const Split = (props: BaseToolProps) => {
const { t } = useTranslation();
@@ -22,23 +20,15 @@ const Split = (props: BaseToolProps) => {
props
);
// Check which split endpoints are available
const allSplitEndpoints = useMemo(() => Object.values(ENDPOINTS), []);
const { endpointStatus } = useMultipleEndpointsEnabled(allSplitEndpoints);
// Filter METHOD_OPTIONS to only show methods with enabled endpoints
const availableMethodOptions = useMemo(() => {
return METHOD_OPTIONS.filter(option => {
const endpoint = ENDPOINTS[option.value];
// If endpoint status is not loaded yet, show all options (optimistic)
// If endpoint is explicitly disabled (false), hide the option
return endpointStatus[endpoint] !== false;
});
}, [endpointStatus]);
const methodTips = useSplitMethodTips();
const settingsTips = useSplitSettingsTips(base.params.parameters.method);
// Get tooltip content for a specific method
const getMethodTooltip = (option: MethodOption) => {
const tooltipContent = useSplitSettingsTips(option.value);
return tooltipContent?.tips || [];
};
// Get the method name for the settings step title
const getSettingsTitle = () => {
if (!base.params.parameters.method) return t("split.steps.settings", "Settings");
@@ -64,9 +54,10 @@ const Split = (props: BaseToolProps) => {
tooltip: methodTips,
content: (
<CardSelector<SplitMethod, MethodOption>
options={availableMethodOptions}
options={METHOD_OPTIONS}
onSelect={(method) => base.params.updateParameter('method', method)}
disabled={base.endpointLoading}
getTooltipContent={getMethodTooltip}
/>
),
},
@@ -6,7 +6,7 @@ import { SetupWizard } from '@app/components/SetupWizard';
import { useFirstLaunchCheck } from '@app/hooks/useFirstLaunchCheck';
import { useBackendInitializer } from '@app/hooks/useBackendInitializer';
import { DESKTOP_DEFAULT_APP_CONFIG } from '@app/config/defaultAppConfig';
import { connectionModeService } from '@app/services/connectionModeService';
import { connectionModeService } from '@desktop/services/connectionModeService';
import { tauriBackendService } from '@app/services/tauriBackendService';
import { authService } from '@app/services/authService';
@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
import { authService, UserInfo } from '@app/services/authService';
import { buildOAuthCallbackHtml } from '@app/utils/oauthCallbackHtml';
import { BASE_PATH } from '@app/constants/app';
import { STIRLING_SAAS_URL } from '@app/constants/connection';
import { STIRLING_SAAS_URL } from '@desktop/constants/connection';
import '@app/routes/authShared/auth.css';
type KnownProviderId = 'google' | 'github' | 'keycloak' | 'azure' | 'apple' | 'oidc';
@@ -69,11 +69,28 @@ export const ServerSelection: React.FC<ServerSelectionProps> = ({ onSelect, load
console.log('[ServerSelection] ✅ Connection test successful');
// Check if the connection requires accepting invalid certificates
const allowInvalidCerts = testResult.requiresInvalidCertAcceptance || false;
if (allowInvalidCerts) {
console.warn('[ServerSelection] ⚠️ Server uses self-signed/invalid certificate - will accept for all requests');
}
// Fetch OAuth providers and check if login is enabled
const enabledProviders: SSOProviderConfig[] = [];
try {
console.log('[ServerSelection] Fetching login configuration...');
const response = await fetch(`${url}/api/v1/proprietary/ui-data/login`);
// Prepare fetch options with danger mode if needed
const fetchOptions: any = {};
if (allowInvalidCerts) {
fetchOptions.danger = {
acceptInvalidCerts: true,
acceptInvalidHostnames: true,
};
console.debug('[ServerSelection] Using danger mode for login config fetch');
}
const response = await fetch(`${url}/api/v1/proprietary/ui-data/login`, fetchOptions);
// Check if security is disabled (status 403, 401, or 404 - endpoint doesn't exist)
if (!response.ok) {
@@ -147,11 +164,12 @@ export const ServerSelection: React.FC<ServerSelectionProps> = ({ onSelect, load
return;
}
// Connection successful - pass URL and OAuth providers
// Connection successful - pass URL, OAuth providers, and cert settings
console.log('[ServerSelection] ✅ Server selection complete, proceeding to login');
onSelect({
url,
enabledOAuthProviders: enabledProviders.length > 0 ? enabledProviders : undefined,
allow_invalid_certs: allowInvalidCerts,
});
} catch (error) {
console.error('[ServerSelection] ❌ Unexpected error during connection test:', error);
@@ -8,7 +8,7 @@ import { SelfHostedLoginScreen } from '@app/components/SetupWizard/SelfHostedLog
import { ServerConfig, connectionModeService } from '@app/services/connectionModeService';
import { AuthServiceError, authService, UserInfo } from '@app/services/authService';
import { tauriBackendService } from '@app/services/tauriBackendService';
import { STIRLING_SAAS_URL } from '@app/constants/connection';
import { STIRLING_SAAS_URL } from '@desktop/constants/connection';
import { listen } from '@tauri-apps/api/event';
import { useEffect } from 'react';
import '@app/routes/authShared/auth.css';

Some files were not shown because too many files have changed in this diff Show More