mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
@@ -29,3 +29,10 @@ project: &project
|
|||||||
- settings.gradle
|
- settings.gradle
|
||||||
- frontend/**
|
- frontend/**
|
||||||
- docker/**
|
- docker/**
|
||||||
|
- testing/**
|
||||||
|
|
||||||
|
frontend: &frontend
|
||||||
|
- frontend/**
|
||||||
|
- .github/workflows/testdriver.yml
|
||||||
|
- testing/**
|
||||||
|
- docker/**
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
pip
|
||||||
|
setuptools
|
||||||
|
WeasyPrint
|
||||||
|
pdf2image
|
||||||
|
pillow
|
||||||
|
unoserver
|
||||||
|
opencv-python-headless
|
||||||
|
pre-commit
|
||||||
@@ -0,0 +1,638 @@
|
|||||||
|
#
|
||||||
|
# This file is autogenerated by pip-compile with Python 3.12
|
||||||
|
# by the following command:
|
||||||
|
#
|
||||||
|
# pip-compile --allow-unsafe --generate-hashes --output-file='.github\scripts\requirements_dev.txt' --strip-extras '.github\scripts\requirements_dev.in'
|
||||||
|
#
|
||||||
|
brotli==1.1.0 \
|
||||||
|
--hash=sha256:03d20af184290887bdea3f0f78c4f737d126c74dc2f3ccadf07e54ceca3bf208 \
|
||||||
|
--hash=sha256:0541e747cce78e24ea12d69176f6a7ddb690e62c425e01d31cc065e69ce55b48 \
|
||||||
|
--hash=sha256:069a121ac97412d1fe506da790b3e69f52254b9df4eb665cd42460c837193354 \
|
||||||
|
--hash=sha256:0737ddb3068957cf1b054899b0883830bb1fec522ec76b1098f9b6e0f02d9419 \
|
||||||
|
--hash=sha256:0b63b949ff929fbc2d6d3ce0e924c9b93c9785d877a21a1b678877ffbbc4423a \
|
||||||
|
--hash=sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128 \
|
||||||
|
--hash=sha256:11d00ed0a83fa22d29bc6b64ef636c4552ebafcef57154b4ddd132f5638fbd1c \
|
||||||
|
--hash=sha256:141bd4d93984070e097521ed07e2575b46f817d08f9fa42b16b9b5f27b5ac088 \
|
||||||
|
--hash=sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9 \
|
||||||
|
--hash=sha256:1ab4fbee0b2d9098c74f3057b2bc055a8bd92ccf02f65944a241b4349229185a \
|
||||||
|
--hash=sha256:1ae56aca0402a0f9a3431cddda62ad71666ca9d4dc3a10a142b9dce2e3c0cda3 \
|
||||||
|
--hash=sha256:1b2c248cd517c222d89e74669a4adfa5577e06ab68771a529060cf5a156e9757 \
|
||||||
|
--hash=sha256:1e9a65b5736232e7a7f91ff3d02277f11d339bf34099a56cdab6a8b3410a02b2 \
|
||||||
|
--hash=sha256:224e57f6eac61cc449f498cc5f0e1725ba2071a3d4f48d5d9dffba42db196438 \
|
||||||
|
--hash=sha256:22fc2a8549ffe699bfba2256ab2ed0421a7b8fadff114a3d201794e45a9ff578 \
|
||||||
|
--hash=sha256:23032ae55523cc7bccb4f6a0bf368cd25ad9bcdcc1990b64a647e7bbcce9cb5b \
|
||||||
|
--hash=sha256:2333e30a5e00fe0fe55903c8832e08ee9c3b1382aacf4db26664a16528d51b4b \
|
||||||
|
--hash=sha256:2954c1c23f81c2eaf0b0717d9380bd348578a94161a65b3a2afc62c86467dd68 \
|
||||||
|
--hash=sha256:2a24c50840d89ded6c9a8fdc7b6ed3692ed4e86f1c4a4a938e1e92def92933e0 \
|
||||||
|
--hash=sha256:2de9d02f5bda03d27ede52e8cfe7b865b066fa49258cbab568720aa5be80a47d \
|
||||||
|
--hash=sha256:2feb1d960f760a575dbc5ab3b1c00504b24caaf6986e2dc2b01c09c87866a943 \
|
||||||
|
--hash=sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd \
|
||||||
|
--hash=sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409 \
|
||||||
|
--hash=sha256:32d95b80260d79926f5fab3c41701dbb818fde1c9da590e77e571eefd14abe28 \
|
||||||
|
--hash=sha256:38025d9f30cf4634f8309c6874ef871b841eb3c347e90b0851f63d1ded5212da \
|
||||||
|
--hash=sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50 \
|
||||||
|
--hash=sha256:3c0ef38c7a7014ffac184db9e04debe495d317cc9c6fb10071f7fefd93100a4f \
|
||||||
|
--hash=sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0 \
|
||||||
|
--hash=sha256:3ee8a80d67a4334482d9712b8e83ca6b1d9bc7e351931252ebef5d8f7335a547 \
|
||||||
|
--hash=sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180 \
|
||||||
|
--hash=sha256:43395e90523f9c23a3d5bdf004733246fba087f2948f87ab28015f12359ca6a0 \
|
||||||
|
--hash=sha256:43ce1b9935bfa1ede40028054d7f48b5469cd02733a365eec8a329ffd342915d \
|
||||||
|
--hash=sha256:4410f84b33374409552ac9b6903507cdb31cd30d2501fc5ca13d18f73548444a \
|
||||||
|
--hash=sha256:494994f807ba0b92092a163a0a283961369a65f6cbe01e8891132b7a320e61eb \
|
||||||
|
--hash=sha256:4d4a848d1837973bf0f4b5e54e3bec977d99be36a7895c61abb659301b02c112 \
|
||||||
|
--hash=sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc \
|
||||||
|
--hash=sha256:4f3607b129417e111e30637af1b56f24f7a49e64763253bbc275c75fa887d4b2 \
|
||||||
|
--hash=sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265 \
|
||||||
|
--hash=sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327 \
|
||||||
|
--hash=sha256:587ca6d3cef6e4e868102672d3bd9dc9698c309ba56d41c2b9c85bbb903cdb95 \
|
||||||
|
--hash=sha256:58d4b711689366d4a03ac7957ab8c28890415e267f9b6589969e74b6e42225ec \
|
||||||
|
--hash=sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd \
|
||||||
|
--hash=sha256:5dab0844f2cf82be357a0eb11a9087f70c5430b2c241493fc122bb6f2bb0917c \
|
||||||
|
--hash=sha256:5e55da2c8724191e5b557f8e18943b1b4839b8efc3ef60d65985bcf6f587dd38 \
|
||||||
|
--hash=sha256:5eeb539606f18a0b232d4ba45adccde4125592f3f636a6182b4a8a436548b914 \
|
||||||
|
--hash=sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0 \
|
||||||
|
--hash=sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a \
|
||||||
|
--hash=sha256:6172447e1b368dcbc458925e5ddaf9113477b0ed542df258d84fa28fc45ceea7 \
|
||||||
|
--hash=sha256:6967ced6730aed543b8673008b5a391c3b1076d834ca438bbd70635c73775368 \
|
||||||
|
--hash=sha256:6974f52a02321b36847cd19d1b8e381bf39939c21efd6ee2fc13a28b0d99348c \
|
||||||
|
--hash=sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0 \
|
||||||
|
--hash=sha256:6c6e0c425f22c1c719c42670d561ad682f7bfeeef918edea971a79ac5252437f \
|
||||||
|
--hash=sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451 \
|
||||||
|
--hash=sha256:7905193081db9bfa73b1219140b3d315831cbff0d8941f22da695832f0dd188f \
|
||||||
|
--hash=sha256:7bc37c4d6b87fb1017ea28c9508b36bbcb0c3d18b4260fcdf08b200c74a6aee8 \
|
||||||
|
--hash=sha256:7c4855522edb2e6ae7fdb58e07c3ba9111e7621a8956f481c68d5d979c93032e \
|
||||||
|
--hash=sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248 \
|
||||||
|
--hash=sha256:7eedaa5d036d9336c95915035fb57422054014ebdeb6f3b42eac809928e40d0c \
|
||||||
|
--hash=sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91 \
|
||||||
|
--hash=sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724 \
|
||||||
|
--hash=sha256:832436e59afb93e1836081a20f324cb185836c617659b07b129141a8426973c7 \
|
||||||
|
--hash=sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966 \
|
||||||
|
--hash=sha256:87a3044c3a35055527ac75e419dfa9f4f3667a1e887ee80360589eb8c90aabb9 \
|
||||||
|
--hash=sha256:890b5a14ce214389b2cc36ce82f3093f96f4cc730c1cffdbefff77a7c71f2a97 \
|
||||||
|
--hash=sha256:89f4988c7203739d48c6f806f1e87a1d96e0806d44f0fba61dba81392c9e474d \
|
||||||
|
--hash=sha256:8bf32b98b75c13ec7cf774164172683d6e7891088f6316e54425fde1efc276d5 \
|
||||||
|
--hash=sha256:8dadd1314583ec0bf2d1379f7008ad627cd6336625d6679cf2f8e67081b83acf \
|
||||||
|
--hash=sha256:901032ff242d479a0efa956d853d16875d42157f98951c0230f69e69f9c09bac \
|
||||||
|
--hash=sha256:9011560a466d2eb3f5a6e4929cf4a09be405c64154e12df0dd72713f6500e32b \
|
||||||
|
--hash=sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951 \
|
||||||
|
--hash=sha256:919e32f147ae93a09fe064d77d5ebf4e35502a8df75c29fb05788528e330fe74 \
|
||||||
|
--hash=sha256:91d7cc2a76b5567591d12c01f019dd7afce6ba8cba6571187e21e2fc418ae648 \
|
||||||
|
--hash=sha256:929811df5462e182b13920da56c6e0284af407d1de637d8e536c5cd00a7daf60 \
|
||||||
|
--hash=sha256:949f3b7c29912693cee0afcf09acd6ebc04c57af949d9bf77d6101ebb61e388c \
|
||||||
|
--hash=sha256:a090ca607cbb6a34b0391776f0cb48062081f5f60ddcce5d11838e67a01928d1 \
|
||||||
|
--hash=sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8 \
|
||||||
|
--hash=sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d \
|
||||||
|
--hash=sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc \
|
||||||
|
--hash=sha256:a469274ad18dc0e4d316eefa616d1d0c2ff9da369af19fa6f3daa4f09671fd61 \
|
||||||
|
--hash=sha256:a599669fd7c47233438a56936988a2478685e74854088ef5293802123b5b2460 \
|
||||||
|
--hash=sha256:a743e5a28af5f70f9c080380a5f908d4d21d40e8f0e0c8901604d15cfa9ba751 \
|
||||||
|
--hash=sha256:a77def80806c421b4b0af06f45d65a136e7ac0bdca3c09d9e2ea4e515367c7e9 \
|
||||||
|
--hash=sha256:a7e53012d2853a07a4a79c00643832161a910674a893d296c9f1259859a289d2 \
|
||||||
|
--hash=sha256:a93dde851926f4f2678e704fadeb39e16c35d8baebd5252c9fd94ce8ce68c4a0 \
|
||||||
|
--hash=sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1 \
|
||||||
|
--hash=sha256:ae15b066e5ad21366600ebec29a7ccbc86812ed267e4b28e860b8ca16a2bc474 \
|
||||||
|
--hash=sha256:aea440a510e14e818e67bfc4027880e2fb500c2ccb20ab21c7a7c8b5b4703d75 \
|
||||||
|
--hash=sha256:af6fa6817889314555aede9a919612b23739395ce767fe7fcbea9a80bf140fe5 \
|
||||||
|
--hash=sha256:b760c65308ff1e462f65d69c12e4ae085cff3b332d894637f6273a12a482d09f \
|
||||||
|
--hash=sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2 \
|
||||||
|
--hash=sha256:c247dd99d39e0338a604f8c2b3bc7061d5c2e9e2ac7ba9cc1be5a69cb6cd832f \
|
||||||
|
--hash=sha256:c5529b34c1c9d937168297f2c1fde7ebe9ebdd5e121297ff9c043bdb2ae3d6fb \
|
||||||
|
--hash=sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6 \
|
||||||
|
--hash=sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9 \
|
||||||
|
--hash=sha256:ca63e1890ede90b2e4454f9a65135a4d387a4585ff8282bb72964fab893f2111 \
|
||||||
|
--hash=sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2 \
|
||||||
|
--hash=sha256:cb1dac1770878ade83f2ccdf7d25e494f05c9165f5246b46a621cc849341dc01 \
|
||||||
|
--hash=sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467 \
|
||||||
|
--hash=sha256:cdbc1fc1bc0bff1cef838eafe581b55bfbffaed4ed0318b724d0b71d4d377619 \
|
||||||
|
--hash=sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf \
|
||||||
|
--hash=sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408 \
|
||||||
|
--hash=sha256:d143fd47fad1db3d7c27a1b1d66162e855b5d50a89666af46e1679c496e8e579 \
|
||||||
|
--hash=sha256:d192f0f30804e55db0d0e0a35d83a9fead0e9a359a9ed0285dbacea60cc10a84 \
|
||||||
|
--hash=sha256:d2b35ca2c7f81d173d2fadc2f4f31e88cc5f7a39ae5b6db5513cf3383b0e0ec7 \
|
||||||
|
--hash=sha256:d342778ef319e1026af243ed0a07c97acf3bad33b9f29e7ae6a1f68fd083e90c \
|
||||||
|
--hash=sha256:d487f5432bf35b60ed625d7e1b448e2dc855422e87469e3f450aa5552b0eb284 \
|
||||||
|
--hash=sha256:d7702622a8b40c49bffb46e1e3ba2e81268d5c04a34f460978c6b5517a34dd52 \
|
||||||
|
--hash=sha256:db85ecf4e609a48f4b29055f1e144231b90edc90af7481aa731ba2d059226b1b \
|
||||||
|
--hash=sha256:de6551e370ef19f8de1807d0a9aa2cdfdce2e85ce88b122fe9f6b2b076837e59 \
|
||||||
|
--hash=sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752 \
|
||||||
|
--hash=sha256:e4fe605b917c70283db7dfe5ada75e04561479075761a0b3866c081d035b01c1 \
|
||||||
|
--hash=sha256:e6a904cb26bfefc2f0a6f240bdf5233be78cd2488900a2f846f3c3ac8489ab80 \
|
||||||
|
--hash=sha256:e79e6520141d792237c70bcd7a3b122d00f2613769ae0cb61c52e89fd3443839 \
|
||||||
|
--hash=sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0 \
|
||||||
|
--hash=sha256:e93dfc1a1165e385cc8239fab7c036fb2cd8093728cbd85097b284d7b99249a2 \
|
||||||
|
--hash=sha256:efa8b278894b14d6da122a72fefcebc28445f2d3f880ac59d46c90f4c13be9a3 \
|
||||||
|
--hash=sha256:f0d8a7a6b5983c2496e364b969f0e526647a06b075d034f3297dc66f3b360c64 \
|
||||||
|
--hash=sha256:f0db75f47be8b8abc8d9e31bc7aad0547ca26f24a54e6fd10231d623f183d089 \
|
||||||
|
--hash=sha256:f296c40e23065d0d6650c4aefe7470d2a25fffda489bcc3eb66083f3ac9f6643 \
|
||||||
|
--hash=sha256:f31859074d57b4639318523d6ffdca586ace54271a73ad23ad021acd807eb14b \
|
||||||
|
--hash=sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e \
|
||||||
|
--hash=sha256:f733d788519c7e3e71f0855c96618720f5d3d60c3cb829d8bbb722dddce37985 \
|
||||||
|
--hash=sha256:fce1473f3ccc4187f75b4690cfc922628aed4d3dd013d047f95a9b3919a86596 \
|
||||||
|
--hash=sha256:fd5f17ff8f14003595ab414e45fce13d073e0762394f957182e69035c9f3d7c2 \
|
||||||
|
--hash=sha256:fdc3ff3bfccdc6b9cc7c342c03aa2400683f0cb891d46e94b64a197910dc4064
|
||||||
|
# via fonttools
|
||||||
|
cffi==1.17.1 \
|
||||||
|
--hash=sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8 \
|
||||||
|
--hash=sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2 \
|
||||||
|
--hash=sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1 \
|
||||||
|
--hash=sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15 \
|
||||||
|
--hash=sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36 \
|
||||||
|
--hash=sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824 \
|
||||||
|
--hash=sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8 \
|
||||||
|
--hash=sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36 \
|
||||||
|
--hash=sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17 \
|
||||||
|
--hash=sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf \
|
||||||
|
--hash=sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc \
|
||||||
|
--hash=sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3 \
|
||||||
|
--hash=sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed \
|
||||||
|
--hash=sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702 \
|
||||||
|
--hash=sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1 \
|
||||||
|
--hash=sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8 \
|
||||||
|
--hash=sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903 \
|
||||||
|
--hash=sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6 \
|
||||||
|
--hash=sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d \
|
||||||
|
--hash=sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b \
|
||||||
|
--hash=sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e \
|
||||||
|
--hash=sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be \
|
||||||
|
--hash=sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c \
|
||||||
|
--hash=sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683 \
|
||||||
|
--hash=sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9 \
|
||||||
|
--hash=sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c \
|
||||||
|
--hash=sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8 \
|
||||||
|
--hash=sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1 \
|
||||||
|
--hash=sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4 \
|
||||||
|
--hash=sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655 \
|
||||||
|
--hash=sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67 \
|
||||||
|
--hash=sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595 \
|
||||||
|
--hash=sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0 \
|
||||||
|
--hash=sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65 \
|
||||||
|
--hash=sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41 \
|
||||||
|
--hash=sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6 \
|
||||||
|
--hash=sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401 \
|
||||||
|
--hash=sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6 \
|
||||||
|
--hash=sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3 \
|
||||||
|
--hash=sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16 \
|
||||||
|
--hash=sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93 \
|
||||||
|
--hash=sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e \
|
||||||
|
--hash=sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4 \
|
||||||
|
--hash=sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964 \
|
||||||
|
--hash=sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c \
|
||||||
|
--hash=sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576 \
|
||||||
|
--hash=sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0 \
|
||||||
|
--hash=sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3 \
|
||||||
|
--hash=sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662 \
|
||||||
|
--hash=sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3 \
|
||||||
|
--hash=sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff \
|
||||||
|
--hash=sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5 \
|
||||||
|
--hash=sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd \
|
||||||
|
--hash=sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f \
|
||||||
|
--hash=sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5 \
|
||||||
|
--hash=sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14 \
|
||||||
|
--hash=sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d \
|
||||||
|
--hash=sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9 \
|
||||||
|
--hash=sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7 \
|
||||||
|
--hash=sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382 \
|
||||||
|
--hash=sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a \
|
||||||
|
--hash=sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e \
|
||||||
|
--hash=sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a \
|
||||||
|
--hash=sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4 \
|
||||||
|
--hash=sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99 \
|
||||||
|
--hash=sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87 \
|
||||||
|
--hash=sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b
|
||||||
|
# via weasyprint
|
||||||
|
cfgv==3.4.0 \
|
||||||
|
--hash=sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9 \
|
||||||
|
--hash=sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560
|
||||||
|
# via pre-commit
|
||||||
|
cssselect2==0.8.0 \
|
||||||
|
--hash=sha256:46fc70ebc41ced7a32cd42d58b1884d72ade23d21e5a4eaaf022401c13f0e76e \
|
||||||
|
--hash=sha256:7674ffb954a3b46162392aee2a3a0aedb2e14ecf99fcc28644900f4e6e3e9d3a
|
||||||
|
# via weasyprint
|
||||||
|
distlib==0.4.0 \
|
||||||
|
--hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \
|
||||||
|
--hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d
|
||||||
|
# via virtualenv
|
||||||
|
filelock==3.18.0 \
|
||||||
|
--hash=sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2 \
|
||||||
|
--hash=sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de
|
||||||
|
# via virtualenv
|
||||||
|
fonttools==4.59.0 \
|
||||||
|
--hash=sha256:052444a5d0151878e87e3e512a1aa1a0ab35ee4c28afde0a778e23b0ace4a7de \
|
||||||
|
--hash=sha256:169b99a2553a227f7b5fea8d9ecd673aa258617f466b2abc6091fe4512a0dcd0 \
|
||||||
|
--hash=sha256:209b75943d158f610b78320eacb5539aa9e920bee2c775445b2846c65d20e19d \
|
||||||
|
--hash=sha256:21e606b2d38fed938dde871c5736822dd6bda7a4631b92e509a1f5cd1b90c5df \
|
||||||
|
--hash=sha256:241313683afd3baacb32a6bd124d0bce7404bc5280e12e291bae1b9bba28711d \
|
||||||
|
--hash=sha256:26731739daa23b872643f0e4072d5939960237d540c35c14e6a06d47d71ca8fe \
|
||||||
|
--hash=sha256:2e7cf8044ce2598bb87e44ba1d2c6e45d7a8decf56055b92906dc53f67c76d64 \
|
||||||
|
--hash=sha256:31003b6a10f70742a63126b80863ab48175fb8272a18ca0846c0482968f0588e \
|
||||||
|
--hash=sha256:332bfe685d1ac58ca8d62b8d6c71c2e52a6c64bc218dc8f7825c9ea51385aa01 \
|
||||||
|
--hash=sha256:37c377f7cb2ab2eca8a0b319c68146d34a339792f9420fca6cd49cf28d370705 \
|
||||||
|
--hash=sha256:37e01c6ec0c98599778c2e688350d624fa4770fbd6144551bd5e032f1199171c \
|
||||||
|
--hash=sha256:401b1941ce37e78b8fd119b419b617277c65ae9417742a63282257434fd68ea2 \
|
||||||
|
--hash=sha256:4536f2695fe5c1ffb528d84a35a7d3967e5558d2af58b4775e7ab1449d65767b \
|
||||||
|
--hash=sha256:4c908a7036f0f3677f8afa577bcd973e3e20ddd2f7c42a33208d18bee95cdb6f \
|
||||||
|
--hash=sha256:51ab1ff33c19e336c02dee1e9fd1abd974a4ca3d8f7eef2a104d0816a241ce97 \
|
||||||
|
--hash=sha256:524133c1be38445c5c0575eacea42dbd44374b310b1ffc4b60ff01d881fabb96 \
|
||||||
|
--hash=sha256:57bb7e26928573ee7c6504f54c05860d867fd35e675769f3ce01b52af38d48e2 \
|
||||||
|
--hash=sha256:60f6665579e909b618282f3c14fa0b80570fbf1ee0e67678b9a9d43aa5d67a37 \
|
||||||
|
--hash=sha256:62224a9bb85b4b66d1b46d45cbe43d71cbf8f527d332b177e3b96191ffbc1e64 \
|
||||||
|
--hash=sha256:6770d7da00f358183d8fd5c4615436189e4f683bdb6affb02cad3d221d7bb757 \
|
||||||
|
--hash=sha256:6801aeddb6acb2c42eafa45bc1cb98ba236871ae6f33f31e984670b749a8e58e \
|
||||||
|
--hash=sha256:70d6b3ceaa9cc5a6ac52884f3b3d9544e8e231e95b23f138bdb78e6d4dc0eae3 \
|
||||||
|
--hash=sha256:78813b49d749e1bb4db1c57f2d4d7e6db22c253cb0a86ad819f5dc197710d4b2 \
|
||||||
|
--hash=sha256:841b2186adce48903c0fef235421ae21549020eca942c1da773ac380b056ab3c \
|
||||||
|
--hash=sha256:84fc186980231a287b28560d3123bd255d3c6b6659828c642b4cf961e2b923d0 \
|
||||||
|
--hash=sha256:885bde7d26e5b40e15c47bd5def48b38cbd50830a65f98122a8fb90962af7cd1 \
|
||||||
|
--hash=sha256:8b4309a2775e4feee7356e63b163969a215d663399cce1b3d3b65e7ec2d9680e \
|
||||||
|
--hash=sha256:8d77f92438daeaddc05682f0f3dac90c5b9829bcac75b57e8ce09cb67786073c \
|
||||||
|
--hash=sha256:902425f5afe28572d65d2bf9c33edd5265c612ff82c69e6f83ea13eafc0dcbea \
|
||||||
|
--hash=sha256:9bcc1e77fbd1609198966ded6b2a9897bd6c6bcbd2287a2fc7d75f1a254179c5 \
|
||||||
|
--hash=sha256:a408c3c51358c89b29cfa5317cf11518b7ce5de1717abb55c5ae2d2921027de6 \
|
||||||
|
--hash=sha256:a9bf8adc9e1f3012edc8f09b08336272aec0c55bc677422273e21280db748f7c \
|
||||||
|
--hash=sha256:b818db35879d2edf7f46c7e729c700a0bce03b61b9412f5a7118406687cb151d \
|
||||||
|
--hash=sha256:b8974b2a266b54c96709bd5e239979cddfd2dbceed331aa567ea1d7c4a2202db \
|
||||||
|
--hash=sha256:be392ec3529e2f57faa28709d60723a763904f71a2b63aabe14fee6648fe3b14 \
|
||||||
|
--hash=sha256:d3972b13148c1d1fbc092b27678a33b3080d1ac0ca305742b0119b75f9e87e38 \
|
||||||
|
--hash=sha256:d40dcf533ca481355aa7b682e9e079f766f35715defa4929aeb5597f9604272e \
|
||||||
|
--hash=sha256:e93df708c69a193fc7987192f94df250f83f3851fda49413f02ba5dded639482 \
|
||||||
|
--hash=sha256:efd7e6660674e234e29937bc1481dceb7e0336bfae75b856b4fb272b5093c5d4 \
|
||||||
|
--hash=sha256:f9b3a78f69dcbd803cf2fb3f972779875b244c1115481dfbdd567b2c22b31f6b \
|
||||||
|
--hash=sha256:fa39475eaccb98f9199eccfda4298abaf35ae0caec676ffc25b3a5e224044464 \
|
||||||
|
--hash=sha256:fbce6dae41b692a5973d0f2158f782b9ad05babc2c2019a970a1094a23909b1b
|
||||||
|
# via weasyprint
|
||||||
|
identify==2.6.13 \
|
||||||
|
--hash=sha256:60381139b3ae39447482ecc406944190f690d4a2997f2584062089848361b33b \
|
||||||
|
--hash=sha256:da8d6c828e773620e13bfa86ea601c5a5310ba4bcd65edf378198b56a1f9fb32
|
||||||
|
# via pre-commit
|
||||||
|
nodeenv==1.9.1 \
|
||||||
|
--hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \
|
||||||
|
--hash=sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9
|
||||||
|
# via pre-commit
|
||||||
|
numpy==2.2.6 \
|
||||||
|
--hash=sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff \
|
||||||
|
--hash=sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47 \
|
||||||
|
--hash=sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84 \
|
||||||
|
--hash=sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d \
|
||||||
|
--hash=sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6 \
|
||||||
|
--hash=sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f \
|
||||||
|
--hash=sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b \
|
||||||
|
--hash=sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49 \
|
||||||
|
--hash=sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163 \
|
||||||
|
--hash=sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571 \
|
||||||
|
--hash=sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42 \
|
||||||
|
--hash=sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff \
|
||||||
|
--hash=sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491 \
|
||||||
|
--hash=sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4 \
|
||||||
|
--hash=sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566 \
|
||||||
|
--hash=sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf \
|
||||||
|
--hash=sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40 \
|
||||||
|
--hash=sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd \
|
||||||
|
--hash=sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06 \
|
||||||
|
--hash=sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282 \
|
||||||
|
--hash=sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680 \
|
||||||
|
--hash=sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db \
|
||||||
|
--hash=sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3 \
|
||||||
|
--hash=sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90 \
|
||||||
|
--hash=sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1 \
|
||||||
|
--hash=sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289 \
|
||||||
|
--hash=sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab \
|
||||||
|
--hash=sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c \
|
||||||
|
--hash=sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d \
|
||||||
|
--hash=sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb \
|
||||||
|
--hash=sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d \
|
||||||
|
--hash=sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a \
|
||||||
|
--hash=sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf \
|
||||||
|
--hash=sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1 \
|
||||||
|
--hash=sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2 \
|
||||||
|
--hash=sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a \
|
||||||
|
--hash=sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543 \
|
||||||
|
--hash=sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00 \
|
||||||
|
--hash=sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c \
|
||||||
|
--hash=sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f \
|
||||||
|
--hash=sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd \
|
||||||
|
--hash=sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868 \
|
||||||
|
--hash=sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303 \
|
||||||
|
--hash=sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83 \
|
||||||
|
--hash=sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3 \
|
||||||
|
--hash=sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d \
|
||||||
|
--hash=sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87 \
|
||||||
|
--hash=sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa \
|
||||||
|
--hash=sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f \
|
||||||
|
--hash=sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae \
|
||||||
|
--hash=sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda \
|
||||||
|
--hash=sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915 \
|
||||||
|
--hash=sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249 \
|
||||||
|
--hash=sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de \
|
||||||
|
--hash=sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8
|
||||||
|
# via opencv-python-headless
|
||||||
|
opencv-python-headless==4.12.0.88 \
|
||||||
|
--hash=sha256:1e58d664809b3350c1123484dd441e1667cd7bed3086db1b9ea1b6f6cb20b50e \
|
||||||
|
--hash=sha256:236c8df54a90f4d02076e6f9c1cc763d794542e886c576a6fee46ec8ff75a7a9 \
|
||||||
|
--hash=sha256:365bb2e486b50feffc2d07a405b953a8f3e8eaa63865bc650034e5c71e7a5154 \
|
||||||
|
--hash=sha256:86b413bdd6c6bf497832e346cd5371995de148e579b9774f8eba686dee3f5528 \
|
||||||
|
--hash=sha256:aeb4b13ecb8b4a0beb2668ea07928160ea7c2cd2d9b5ef571bbee6bafe9cc8d0 \
|
||||||
|
--hash=sha256:cfdc017ddf2e59b6c2f53bc12d74b6b0be7ded4ec59083ea70763921af2b6c09 \
|
||||||
|
--hash=sha256:fde2cf5c51e4def5f2132d78e0c08f9c14783cd67356922182c6845b9af87dbd
|
||||||
|
# via -r .github/scripts/requirements_dev.in
|
||||||
|
pdf2image==1.17.0 \
|
||||||
|
--hash=sha256:eaa959bc116b420dd7ec415fcae49b98100dda3dd18cd2fdfa86d09f112f6d57 \
|
||||||
|
--hash=sha256:ecdd58d7afb810dffe21ef2b1bbc057ef434dabbac6c33778a38a3f7744a27e2
|
||||||
|
# via -r .github/scripts/requirements_dev.in
|
||||||
|
pillow==11.3.0 \
|
||||||
|
--hash=sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2 \
|
||||||
|
--hash=sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214 \
|
||||||
|
--hash=sha256:040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e \
|
||||||
|
--hash=sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59 \
|
||||||
|
--hash=sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50 \
|
||||||
|
--hash=sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632 \
|
||||||
|
--hash=sha256:092c80c76635f5ecb10f3f83d76716165c96f5229addbd1ec2bdbbda7d496e06 \
|
||||||
|
--hash=sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a \
|
||||||
|
--hash=sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51 \
|
||||||
|
--hash=sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced \
|
||||||
|
--hash=sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f \
|
||||||
|
--hash=sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12 \
|
||||||
|
--hash=sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8 \
|
||||||
|
--hash=sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6 \
|
||||||
|
--hash=sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580 \
|
||||||
|
--hash=sha256:19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f \
|
||||||
|
--hash=sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac \
|
||||||
|
--hash=sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860 \
|
||||||
|
--hash=sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd \
|
||||||
|
--hash=sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722 \
|
||||||
|
--hash=sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8 \
|
||||||
|
--hash=sha256:23cff760a9049c502721bdb743a7cb3e03365fafcdfc2ef9784610714166e5a4 \
|
||||||
|
--hash=sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673 \
|
||||||
|
--hash=sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788 \
|
||||||
|
--hash=sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542 \
|
||||||
|
--hash=sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e \
|
||||||
|
--hash=sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd \
|
||||||
|
--hash=sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8 \
|
||||||
|
--hash=sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523 \
|
||||||
|
--hash=sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967 \
|
||||||
|
--hash=sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809 \
|
||||||
|
--hash=sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477 \
|
||||||
|
--hash=sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027 \
|
||||||
|
--hash=sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae \
|
||||||
|
--hash=sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b \
|
||||||
|
--hash=sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c \
|
||||||
|
--hash=sha256:48d254f8a4c776de343051023eb61ffe818299eeac478da55227d96e241de53f \
|
||||||
|
--hash=sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e \
|
||||||
|
--hash=sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b \
|
||||||
|
--hash=sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7 \
|
||||||
|
--hash=sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27 \
|
||||||
|
--hash=sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361 \
|
||||||
|
--hash=sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae \
|
||||||
|
--hash=sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d \
|
||||||
|
--hash=sha256:6359a3bc43f57d5b375d1ad54a0074318a0844d11b76abccf478c37c986d3cfc \
|
||||||
|
--hash=sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58 \
|
||||||
|
--hash=sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad \
|
||||||
|
--hash=sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6 \
|
||||||
|
--hash=sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024 \
|
||||||
|
--hash=sha256:6a418691000f2a418c9135a7cf0d797c1bb7d9a485e61fe8e7722845b95ef978 \
|
||||||
|
--hash=sha256:6abdbfd3aea42be05702a8dd98832329c167ee84400a1d1f61ab11437f1717eb \
|
||||||
|
--hash=sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d \
|
||||||
|
--hash=sha256:7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0 \
|
||||||
|
--hash=sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9 \
|
||||||
|
--hash=sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f \
|
||||||
|
--hash=sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874 \
|
||||||
|
--hash=sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa \
|
||||||
|
--hash=sha256:7aee118e30a4cf54fdd873bd3a29de51e29105ab11f9aad8c32123f58c8f8081 \
|
||||||
|
--hash=sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149 \
|
||||||
|
--hash=sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6 \
|
||||||
|
--hash=sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d \
|
||||||
|
--hash=sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd \
|
||||||
|
--hash=sha256:819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f \
|
||||||
|
--hash=sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c \
|
||||||
|
--hash=sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31 \
|
||||||
|
--hash=sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e \
|
||||||
|
--hash=sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db \
|
||||||
|
--hash=sha256:89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6 \
|
||||||
|
--hash=sha256:8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f \
|
||||||
|
--hash=sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494 \
|
||||||
|
--hash=sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69 \
|
||||||
|
--hash=sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94 \
|
||||||
|
--hash=sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77 \
|
||||||
|
--hash=sha256:97afb3a00b65cc0804d1c7abddbf090a81eaac02768af58cbdcaaa0a931e0b6d \
|
||||||
|
--hash=sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7 \
|
||||||
|
--hash=sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a \
|
||||||
|
--hash=sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438 \
|
||||||
|
--hash=sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288 \
|
||||||
|
--hash=sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b \
|
||||||
|
--hash=sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635 \
|
||||||
|
--hash=sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3 \
|
||||||
|
--hash=sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d \
|
||||||
|
--hash=sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe \
|
||||||
|
--hash=sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0 \
|
||||||
|
--hash=sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe \
|
||||||
|
--hash=sha256:be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a \
|
||||||
|
--hash=sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805 \
|
||||||
|
--hash=sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8 \
|
||||||
|
--hash=sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36 \
|
||||||
|
--hash=sha256:cadc9e0ea0a2431124cde7e1697106471fc4c1da01530e679b2391c37d3fbb3a \
|
||||||
|
--hash=sha256:cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b \
|
||||||
|
--hash=sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e \
|
||||||
|
--hash=sha256:d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25 \
|
||||||
|
--hash=sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12 \
|
||||||
|
--hash=sha256:e5c5858ad8ec655450a7c7df532e9842cf8df7cc349df7225c60d5d348c8aada \
|
||||||
|
--hash=sha256:e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c \
|
||||||
|
--hash=sha256:ea944117a7974ae78059fcc1800e5d3295172bb97035c0c1d9345fca1419da71 \
|
||||||
|
--hash=sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d \
|
||||||
|
--hash=sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c \
|
||||||
|
--hash=sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6 \
|
||||||
|
--hash=sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1 \
|
||||||
|
--hash=sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50 \
|
||||||
|
--hash=sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653 \
|
||||||
|
--hash=sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c \
|
||||||
|
--hash=sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4 \
|
||||||
|
--hash=sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3
|
||||||
|
# via
|
||||||
|
# -r .github/scripts/requirements_dev.in
|
||||||
|
# pdf2image
|
||||||
|
# weasyprint
|
||||||
|
platformdirs==4.3.8 \
|
||||||
|
--hash=sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc \
|
||||||
|
--hash=sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4
|
||||||
|
# via virtualenv
|
||||||
|
pre-commit==4.3.0 \
|
||||||
|
--hash=sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8 \
|
||||||
|
--hash=sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16
|
||||||
|
# via -r .github/scripts/requirements_dev.in
|
||||||
|
pycparser==2.22 \
|
||||||
|
--hash=sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6 \
|
||||||
|
--hash=sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc
|
||||||
|
# via cffi
|
||||||
|
pydyf==0.11.0 \
|
||||||
|
--hash=sha256:0aaf9e2ebbe786ec7a78ec3fbffa4cdcecde53fd6f563221d53c6bc1328848a3 \
|
||||||
|
--hash=sha256:394dddf619cca9d0c55715e3c55ea121a9bf9cbc780cdc1201a2427917b86b64
|
||||||
|
# via weasyprint
|
||||||
|
pyphen==0.17.2 \
|
||||||
|
--hash=sha256:3a07fb017cb2341e1d9ff31b8634efb1ae4dc4b130468c7c39dd3d32e7c3affd \
|
||||||
|
--hash=sha256:f60647a9c9b30ec6c59910097af82bc5dd2d36576b918e44148d8b07ef3b4aa3
|
||||||
|
# via weasyprint
|
||||||
|
pyyaml==6.0.2 \
|
||||||
|
--hash=sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff \
|
||||||
|
--hash=sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48 \
|
||||||
|
--hash=sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086 \
|
||||||
|
--hash=sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e \
|
||||||
|
--hash=sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133 \
|
||||||
|
--hash=sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5 \
|
||||||
|
--hash=sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484 \
|
||||||
|
--hash=sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee \
|
||||||
|
--hash=sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5 \
|
||||||
|
--hash=sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68 \
|
||||||
|
--hash=sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a \
|
||||||
|
--hash=sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf \
|
||||||
|
--hash=sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99 \
|
||||||
|
--hash=sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8 \
|
||||||
|
--hash=sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85 \
|
||||||
|
--hash=sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19 \
|
||||||
|
--hash=sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc \
|
||||||
|
--hash=sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a \
|
||||||
|
--hash=sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1 \
|
||||||
|
--hash=sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317 \
|
||||||
|
--hash=sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c \
|
||||||
|
--hash=sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631 \
|
||||||
|
--hash=sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d \
|
||||||
|
--hash=sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652 \
|
||||||
|
--hash=sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5 \
|
||||||
|
--hash=sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e \
|
||||||
|
--hash=sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b \
|
||||||
|
--hash=sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8 \
|
||||||
|
--hash=sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476 \
|
||||||
|
--hash=sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706 \
|
||||||
|
--hash=sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563 \
|
||||||
|
--hash=sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237 \
|
||||||
|
--hash=sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b \
|
||||||
|
--hash=sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083 \
|
||||||
|
--hash=sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180 \
|
||||||
|
--hash=sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425 \
|
||||||
|
--hash=sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e \
|
||||||
|
--hash=sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f \
|
||||||
|
--hash=sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725 \
|
||||||
|
--hash=sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183 \
|
||||||
|
--hash=sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab \
|
||||||
|
--hash=sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774 \
|
||||||
|
--hash=sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725 \
|
||||||
|
--hash=sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e \
|
||||||
|
--hash=sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5 \
|
||||||
|
--hash=sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d \
|
||||||
|
--hash=sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290 \
|
||||||
|
--hash=sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44 \
|
||||||
|
--hash=sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed \
|
||||||
|
--hash=sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4 \
|
||||||
|
--hash=sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba \
|
||||||
|
--hash=sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12 \
|
||||||
|
--hash=sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4
|
||||||
|
# via pre-commit
|
||||||
|
tinycss2==1.4.0 \
|
||||||
|
--hash=sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7 \
|
||||||
|
--hash=sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289
|
||||||
|
# via
|
||||||
|
# cssselect2
|
||||||
|
# weasyprint
|
||||||
|
tinyhtml5==2.0.0 \
|
||||||
|
--hash=sha256:086f998833da24c300c414d9fe81d9b368fd04cb9d2596a008421cbc705fcfcc \
|
||||||
|
--hash=sha256:13683277c5b176d070f82d099d977194b7a1e26815b016114f581a74bbfbf47e
|
||||||
|
# via weasyprint
|
||||||
|
unoserver==3.3.2 \
|
||||||
|
--hash=sha256:1eeb7467cf6b56b8eff3b576e2d1b2b2ff4e0eb2052e995ac80a1456de300639 \
|
||||||
|
--hash=sha256:87e144f903ee21951b2e06a97549450c13ed7eca5bcebad942d3352d4e882616
|
||||||
|
# via -r .github/scripts/requirements_dev.in
|
||||||
|
virtualenv==20.33.1 \
|
||||||
|
--hash=sha256:07c19bc66c11acab6a5958b815cbcee30891cd1c2ccf53785a28651a0d8d8a67 \
|
||||||
|
--hash=sha256:1b44478d9e261b3fb8baa5e74a0ca3bc0e05f21aa36167bf9cbf850e542765b8
|
||||||
|
# via pre-commit
|
||||||
|
weasyprint==66.0 \
|
||||||
|
--hash=sha256:82b0783b726fcd318e2c977dcdddca76515b30044bc7a830cc4fbe717582a6d0 \
|
||||||
|
--hash=sha256:da71dc87dc129ac9cffdc65e5477e90365ab9dbae45c744014ec1d06303dde40
|
||||||
|
# via -r .github/scripts/requirements_dev.in
|
||||||
|
webencodings==0.5.1 \
|
||||||
|
--hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \
|
||||||
|
--hash=sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923
|
||||||
|
# via
|
||||||
|
# cssselect2
|
||||||
|
# tinycss2
|
||||||
|
# tinyhtml5
|
||||||
|
zopfli==0.2.3.post1 \
|
||||||
|
--hash=sha256:0aa5f90d6298bda02a95bc8dc8c3c19004d5a4e44bda00b67ca7431d857b4b54 \
|
||||||
|
--hash=sha256:0cc20b02a9531559945324c38302fd4ba763311632d0ec8a1a0aa9c10ea363e6 \
|
||||||
|
--hash=sha256:1d8cc06605519e82b16df090e17cb3990d1158861b2872c3117f1168777b81e4 \
|
||||||
|
--hash=sha256:1f990634fd5c5c8ced8edddd8bd45fab565123b4194d6841e01811292650acae \
|
||||||
|
--hash=sha256:2345e713260a350bea0b01a816a469ea356bc2d63d009a0d777691ecbbcf7493 \
|
||||||
|
--hash=sha256:2768c877f76c8a0e7519b1c86c93757f3c01492ddde55751e9988afb7eff64e1 \
|
||||||
|
--hash=sha256:29ea74e72ffa6e291b8c6f2504ce6c146b4fe990c724c1450eb8e4c27fd31431 \
|
||||||
|
--hash=sha256:34a99592f3d9eb6f737616b5bd74b48a589fdb3cb59a01a50d636ea81d6af272 \
|
||||||
|
--hash=sha256:3654bfc927bc478b1c3f3ff5056ed7b20a1a37fa108ca503256d0a699c03bbb1 \
|
||||||
|
--hash=sha256:3657e416ffb8f31d9d3424af12122bb251befae109f2e271d87d825c92fc5b7b \
|
||||||
|
--hash=sha256:37d011e92f7b9622742c905fdbed9920a1d0361df84142807ea2a528419dea7f \
|
||||||
|
--hash=sha256:3827170de28faf144992d3d4dcf8f3998fe3c8a6a6f4a08f1d42c2ec6119d2bb \
|
||||||
|
--hash=sha256:39e576f93576c5c223b41d9c780bbb91fd6db4babf3223d2a4fe7bf568e2b5a8 \
|
||||||
|
--hash=sha256:3a89277ed5f8c0fb2d0b46d669aa0633123aa7381f1f6118c12f15e0fb48f8ca \
|
||||||
|
--hash=sha256:3c163911f8bad94b3e1db0a572e7c28ba681a0c91d0002ea1e4fa9264c21ef17 \
|
||||||
|
--hash=sha256:3f0197b6aa6eb3086ae9e66d6dd86c4d502b6c68b0ec490496348ae8c05ecaef \
|
||||||
|
--hash=sha256:48dba9251060289101343110ab47c0756f66f809bb4d1ddbb6d5c7e7752115c5 \
|
||||||
|
--hash=sha256:4915a41375bdee4db749ecd07d985a0486eb688a6619f713b7bf6fbfd145e960 \
|
||||||
|
--hash=sha256:4c1226a7e2c7105ac31503a9bb97454743f55d88164d6d46bc138051b77f609b \
|
||||||
|
--hash=sha256:4e50ffac74842c1c1018b9b73875a0d0a877c066ab06bf7cccbaa84af97e754f \
|
||||||
|
--hash=sha256:518f1f4ed35dd69ce06b552f84e6d081f07c552b4c661c5312d950a0b764a58a \
|
||||||
|
--hash=sha256:5aad740b4d4fcbaaae4887823925166ffd062db3b248b3f432198fc287381d1a \
|
||||||
|
--hash=sha256:5f272186e03ad55e7af09ab78055535c201b1a0bcc2944edb1768298d9c483a4 \
|
||||||
|
--hash=sha256:5fcfc0dc2761e4fcc15ad5d273b4d58c2e8e059d3214a7390d4d3c8e2aee644e \
|
||||||
|
--hash=sha256:60db20f06c3d4c5934b16cfa62a2cc5c3f0686bffe0071ed7804d3c31ab1a04e \
|
||||||
|
--hash=sha256:615a8ac9dda265e9cc38b2a76c3142e4a9f30fea4a79c85f670850783bc6feb4 \
|
||||||
|
--hash=sha256:6482db9876c68faac2d20a96b566ffbf65ddaadd97b222e4e73641f4f8722fc4 \
|
||||||
|
--hash=sha256:6617fb10f9e4393b331941861d73afb119cd847e88e4974bdbe8068ceef3f73f \
|
||||||
|
--hash=sha256:676919fba7311125244eb0c4393679ac5fe856e5864a15d122bd815205369fa0 \
|
||||||
|
--hash=sha256:6c2d2bc8129707e34c51f9352c4636ca313b52350bbb7e04637c46c1818a2a70 \
|
||||||
|
--hash=sha256:71390dbd3fbf6ebea9a5d85ffed8c26ee1453ee09248e9b88486e30e0397b775 \
|
||||||
|
--hash=sha256:716cdbfc57bfd3d3e31a58e6246e8190e6849b7dbb7c4ce39ef8bbf0edb8f6d5 \
|
||||||
|
--hash=sha256:75a26a2307b10745a83b660c404416e984ee6fca515ec7f0765f69af3ce08072 \
|
||||||
|
--hash=sha256:7be5cc6732eb7b4df17305d8a7b293223f934a31783a874a01164703bc1be6cd \
|
||||||
|
--hash=sha256:7cce242b5df12b2b172489daf19c32e5577dd2fac659eb4b17f6a6efb446fd5c \
|
||||||
|
--hash=sha256:81c341d9bb87a6dbbb0d45d6e272aca80c7c97b4b210f9b6e233bf8b87242f29 \
|
||||||
|
--hash=sha256:89899641d4de97dbad8e0cde690040d078b6aea04066dacaab98e0b5a23573f2 \
|
||||||
|
--hash=sha256:8d5ab297d660b75c159190ce6d73035502310e40fd35170aed7d1a1aea7ddd65 \
|
||||||
|
--hash=sha256:8fbe5bcf10d01aab3513550f284c09fef32f342b36f56bfae2120a9c4d12c130 \
|
||||||
|
--hash=sha256:91a2327a4d7e77471fa4fbb26991c6de4a738c6fc6a33e09bb25f56a870a4b7b \
|
||||||
|
--hash=sha256:95a260cafd56b8fffa679918937401c80bb38e1681c448b988022e4c3610965d \
|
||||||
|
--hash=sha256:96484dc0f48be1c5d7ae9f38ed1ce41e3675fd506b27c11a6607f14b49101e99 \
|
||||||
|
--hash=sha256:9a6aec38a989bad7ddd1ef53f1265699e49e294d08231b5313d61293f3cd6237 \
|
||||||
|
--hash=sha256:9ba214f4f45bec195ee8559651154d3ac2932470b9d91c5715fc29c013349f8c \
|
||||||
|
--hash=sha256:9f4a7ec2770e6af05f5a02733fd3900f30a9cd58e5d6d3727e14c5bcd6e7d587 \
|
||||||
|
--hash=sha256:a1cf720896d2ce998bc8e051d4b4ce0d8bec007aab6243102e8e1d22a0b2fb3f \
|
||||||
|
--hash=sha256:a241a68581d34d67b40c425cce3d1fd211c092f99d9250947824ccba9f491949 \
|
||||||
|
--hash=sha256:a53b18797cdef27e019db595d66c4b077325afe2fd62145953275f53d84ce40c \
|
||||||
|
--hash=sha256:a82fc2dbebe6eb908b9c665e71496f8525c1bc4d2e3a7a7722ef2b128b6227c8 \
|
||||||
|
--hash=sha256:a86eb88e06bd87e1fff31dac878965c26b0c26db59ddcf78bb0379a954b120de \
|
||||||
|
--hash=sha256:aa588b21044f8a74e423d8c8a4c7fc9988501878aacced793467010039c50734 \
|
||||||
|
--hash=sha256:b05296e8bc88c92e2b21e0a9bae4740c1551ee613c1d93a51fd28a7a0b2b6fbb \
|
||||||
|
--hash=sha256:b0ec13f352ea5ae0fc91f98a48540512eed0767d0ec4f7f3cb92d92797983d18 \
|
||||||
|
--hash=sha256:b3df42f52502438ee973042cc551877d24619fa1cd38ef7b7e9ac74200daca8b \
|
||||||
|
--hash=sha256:b78008a69300d929ca2efeffec951b64a312e9a811e265ea4a907ab546d79fa6 \
|
||||||
|
--hash=sha256:b9026a21b6d41eb0e2e63f5bc1242c3fcc43ecb770963cda99a4307863dac12e \
|
||||||
|
--hash=sha256:bbe429fc50686bb2a2608a30843e36fbaa123462a5284f136c7d9e0145220bfd \
|
||||||
|
--hash=sha256:bfa1eb759e07d8b7aa7a310a2bc535e127ee70addf90dc8d4b946b593c3e51a8 \
|
||||||
|
--hash=sha256:c1e0ed5d84ffa2d677cc9582fc01e61dab2e7ef8b8996e055f0a76167b1b94df \
|
||||||
|
--hash=sha256:c4278d1873ce6e803e5d4f8d702fd3026bd67fca744aa98881324d1157ddf748 \
|
||||||
|
--hash=sha256:cac2b37ab21c2b36a10b685b1893ebd6b0f83ae26004838ac817680881576567 \
|
||||||
|
--hash=sha256:cbe6df25807227519debd1a57ab236f5f6bad441500e85b13903e51f93a43214 \
|
||||||
|
--hash=sha256:cd2c002f160502608dcc822ed2441a0f4509c52e86fcfd1a09e937278ed1ca14 \
|
||||||
|
--hash=sha256:e0137dd64a493ba6a4be37405cfd6febe650a98cc1e9dca8f6b8c63b1db11b41 \
|
||||||
|
--hash=sha256:e63d558847166543c2c9789e6f985400a520b7eacc4b99181668b2c3aeadd352 \
|
||||||
|
--hash=sha256:eb45a34f23da4f8bc712b6376ca5396914b0b7c09adbb001dad964eb7f3132f8 \
|
||||||
|
--hash=sha256:ecb7572df5372abce8073df078207d9d1749f20b8b136089916a4a0868d56051 \
|
||||||
|
--hash=sha256:f12000a6accdd4bf0a3fa6eaa1b1c7a7bc80af0a2edf3f89d770d3dcce1d0e22 \
|
||||||
|
--hash=sha256:f7d69c1a7168ad0e9cb864e8663acb232986a0c9c9cb9801f56bf6214f53a54d \
|
||||||
|
--hash=sha256:f815fcc2b2a457977724bad97fb4854022980f51ce7b136925e336b530545ae1 \
|
||||||
|
--hash=sha256:fc39f5c27f962ec8660d8d20c24762431131b5d8c672b44b0a54cf2b5bcde9b9
|
||||||
|
# via fonttools
|
||||||
|
|
||||||
|
# The following packages are considered to be unsafe in a requirements file:
|
||||||
|
pip==25.2 \
|
||||||
|
--hash=sha256:578283f006390f85bb6282dffb876454593d637f5d1be494b5202ce4877e71f2 \
|
||||||
|
--hash=sha256:6d67a2b4e7f14d8b31b8b52648866fa717f45a1eb70e83002f4331d07e953717
|
||||||
|
# via -r .github/scripts/requirements_dev.in
|
||||||
|
setuptools==80.9.0 \
|
||||||
|
--hash=sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 \
|
||||||
|
--hash=sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c
|
||||||
|
# via -r .github/scripts/requirements_dev.in
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# This file is autogenerated by pip-compile with Python 3.10
|
# This file is autogenerated by pip-compile with Python 3.12
|
||||||
# by the following command:
|
# by the following command:
|
||||||
#
|
#
|
||||||
# pip-compile --generate-hashes --output-file='.github\scripts\requirements_pre_commit.txt' --strip-extras '.github\scripts\requirements_pre_commit.in'
|
# pip-compile --generate-hashes --output-file='.github\scripts\requirements_pre_commit.txt' --strip-extras '.github\scripts\requirements_pre_commit.in'
|
||||||
@@ -8,86 +8,106 @@ cfgv==3.4.0 \
|
|||||||
--hash=sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9 \
|
--hash=sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9 \
|
||||||
--hash=sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560
|
--hash=sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560
|
||||||
# via pre-commit
|
# via pre-commit
|
||||||
distlib==0.3.9 \
|
distlib==0.4.0 \
|
||||||
--hash=sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87 \
|
--hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \
|
||||||
--hash=sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403
|
--hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d
|
||||||
# via virtualenv
|
# via virtualenv
|
||||||
filelock==3.18.0 \
|
filelock==3.19.1 \
|
||||||
--hash=sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2 \
|
--hash=sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58 \
|
||||||
--hash=sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de
|
--hash=sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d
|
||||||
# via virtualenv
|
# via virtualenv
|
||||||
identify==2.6.12 \
|
identify==2.6.15 \
|
||||||
--hash=sha256:ad9672d5a72e0d2ff7c5c8809b62dfa60458626352fb0eb7b55e69bdc45334a2 \
|
--hash=sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757 \
|
||||||
--hash=sha256:d8de45749f1efb108badef65ee8386f0f7bb19a7f26185f74de6367bffbaf0e6
|
--hash=sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf
|
||||||
# via pre-commit
|
# via pre-commit
|
||||||
nodeenv==1.9.1 \
|
nodeenv==1.9.1 \
|
||||||
--hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \
|
--hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \
|
||||||
--hash=sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9
|
--hash=sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9
|
||||||
# via pre-commit
|
# via pre-commit
|
||||||
platformdirs==4.3.8 \
|
platformdirs==4.4.0 \
|
||||||
--hash=sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc \
|
--hash=sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85 \
|
||||||
--hash=sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4
|
--hash=sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf
|
||||||
# via virtualenv
|
# via virtualenv
|
||||||
pre-commit==4.2.0 \
|
pre-commit==4.3.0 \
|
||||||
--hash=sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146 \
|
--hash=sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8 \
|
||||||
--hash=sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd
|
--hash=sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16
|
||||||
# via -r .github\scripts\requirements_pre_commit.in
|
# via -r .github/scripts/requirements_pre_commit.in
|
||||||
pyyaml==6.0.2 \
|
pyyaml==6.0.3 \
|
||||||
--hash=sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff \
|
--hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \
|
||||||
--hash=sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48 \
|
--hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \
|
||||||
--hash=sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086 \
|
--hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \
|
||||||
--hash=sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e \
|
--hash=sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956 \
|
||||||
--hash=sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133 \
|
--hash=sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6 \
|
||||||
--hash=sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5 \
|
--hash=sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c \
|
||||||
--hash=sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484 \
|
--hash=sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65 \
|
||||||
--hash=sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee \
|
--hash=sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a \
|
||||||
--hash=sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5 \
|
--hash=sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0 \
|
||||||
--hash=sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68 \
|
--hash=sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b \
|
||||||
--hash=sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a \
|
--hash=sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 \
|
||||||
--hash=sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf \
|
--hash=sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6 \
|
||||||
--hash=sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99 \
|
--hash=sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7 \
|
||||||
--hash=sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8 \
|
--hash=sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e \
|
||||||
--hash=sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85 \
|
--hash=sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007 \
|
||||||
--hash=sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19 \
|
--hash=sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310 \
|
||||||
--hash=sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc \
|
--hash=sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4 \
|
||||||
--hash=sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a \
|
--hash=sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9 \
|
||||||
--hash=sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1 \
|
--hash=sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295 \
|
||||||
--hash=sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317 \
|
--hash=sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea \
|
||||||
--hash=sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c \
|
--hash=sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0 \
|
||||||
--hash=sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631 \
|
--hash=sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e \
|
||||||
--hash=sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d \
|
--hash=sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac \
|
||||||
--hash=sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652 \
|
--hash=sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9 \
|
||||||
--hash=sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5 \
|
--hash=sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7 \
|
||||||
--hash=sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e \
|
--hash=sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35 \
|
||||||
--hash=sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b \
|
--hash=sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb \
|
||||||
--hash=sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8 \
|
--hash=sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b \
|
||||||
--hash=sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476 \
|
--hash=sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69 \
|
||||||
--hash=sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706 \
|
--hash=sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5 \
|
||||||
--hash=sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563 \
|
--hash=sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b \
|
||||||
--hash=sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237 \
|
--hash=sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c \
|
||||||
--hash=sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b \
|
--hash=sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369 \
|
||||||
--hash=sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083 \
|
--hash=sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd \
|
||||||
--hash=sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180 \
|
--hash=sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824 \
|
||||||
--hash=sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425 \
|
--hash=sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198 \
|
||||||
--hash=sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e \
|
--hash=sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065 \
|
||||||
--hash=sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f \
|
--hash=sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c \
|
||||||
--hash=sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725 \
|
--hash=sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c \
|
||||||
--hash=sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183 \
|
--hash=sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764 \
|
||||||
--hash=sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab \
|
--hash=sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196 \
|
||||||
--hash=sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774 \
|
--hash=sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b \
|
||||||
--hash=sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725 \
|
--hash=sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00 \
|
||||||
--hash=sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e \
|
--hash=sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac \
|
||||||
--hash=sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5 \
|
--hash=sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8 \
|
||||||
--hash=sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d \
|
--hash=sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e \
|
||||||
--hash=sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290 \
|
--hash=sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 \
|
||||||
--hash=sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44 \
|
--hash=sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3 \
|
||||||
--hash=sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed \
|
--hash=sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5 \
|
||||||
--hash=sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4 \
|
--hash=sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4 \
|
||||||
--hash=sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba \
|
--hash=sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b \
|
||||||
--hash=sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12 \
|
--hash=sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf \
|
||||||
--hash=sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4
|
--hash=sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5 \
|
||||||
|
--hash=sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702 \
|
||||||
|
--hash=sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8 \
|
||||||
|
--hash=sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788 \
|
||||||
|
--hash=sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da \
|
||||||
|
--hash=sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d \
|
||||||
|
--hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc \
|
||||||
|
--hash=sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c \
|
||||||
|
--hash=sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba \
|
||||||
|
--hash=sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f \
|
||||||
|
--hash=sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917 \
|
||||||
|
--hash=sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5 \
|
||||||
|
--hash=sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26 \
|
||||||
|
--hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \
|
||||||
|
--hash=sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b \
|
||||||
|
--hash=sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be \
|
||||||
|
--hash=sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c \
|
||||||
|
--hash=sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3 \
|
||||||
|
--hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \
|
||||||
|
--hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \
|
||||||
|
--hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0
|
||||||
# via pre-commit
|
# via pre-commit
|
||||||
virtualenv==20.31.2 \
|
virtualenv==20.34.0 \
|
||||||
--hash=sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11 \
|
--hash=sha256:341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026 \
|
||||||
--hash=sha256:e10c0a9d02835e592521be48b332b6caee6887f332c111aa79a09b9e79efc2af
|
--hash=sha256:44815b2c9dee7ed86e387b842a84f20b93f7f417f95886ca1996a72a4138eb1a
|
||||||
# via pre-commit
|
# via pre-commit
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# This file is autogenerated by pip-compile with Python 3.10
|
# This file is autogenerated by pip-compile with Python 3.12
|
||||||
# by the following command:
|
# by the following command:
|
||||||
#
|
#
|
||||||
# pip-compile --generate-hashes --output-file='.github\scripts\requirements_sync_readme.txt' --strip-extras '.github\scripts\requirements_sync_readme.in'
|
# pip-compile --generate-hashes --output-file='.github\scripts\requirements_sync_readme.txt' --strip-extras '.github\scripts\requirements_sync_readme.in'
|
||||||
@@ -7,4 +7,4 @@
|
|||||||
tomlkit==0.13.3 \
|
tomlkit==0.13.3 \
|
||||||
--hash=sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1 \
|
--hash=sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1 \
|
||||||
--hash=sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0
|
--hash=sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0
|
||||||
# via -r .github\scripts\requirements_sync_readme.in
|
# via -r .github/scripts/requirements_sync_readme.in
|
||||||
|
|||||||
@@ -34,20 +34,18 @@ jobs:
|
|||||||
)
|
)
|
||||||
outputs:
|
outputs:
|
||||||
pr_number: ${{ steps.get-pr.outputs.pr_number }}
|
pr_number: ${{ steps.get-pr.outputs.pr_number }}
|
||||||
pr_repository: ${{ steps.get-pr-info.outputs.repository }}
|
|
||||||
pr_ref: ${{ steps.get-pr-info.outputs.ref }}
|
|
||||||
comment_id: ${{ github.event.comment.id }}
|
comment_id: ${{ github.event.comment.id }}
|
||||||
disable_security: ${{ steps.check-security-flag.outputs.disable_security }}
|
disable_security: ${{ steps.check-security-flag.outputs.disable_security }}
|
||||||
enable_pro: ${{ steps.check-pro-flag.outputs.enable_pro }}
|
enable_pro: ${{ steps.check-pro-flag.outputs.enable_pro }}
|
||||||
enable_enterprise: ${{ steps.check-pro-flag.outputs.enable_enterprise }}
|
enable_enterprise: ${{ steps.check-pro-flag.outputs.enable_enterprise }}
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Checkout PR
|
- name: Checkout PR
|
||||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Setup GitHub App Bot
|
- name: Setup GitHub App Bot
|
||||||
if: github.actor != 'dependabot[bot]'
|
if: github.actor != 'dependabot[bot]'
|
||||||
@@ -60,36 +58,13 @@ jobs:
|
|||||||
|
|
||||||
- name: Get PR data
|
- name: Get PR data
|
||||||
id: get-pr
|
id: get-pr
|
||||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const prNumber = context.payload.issue.number;
|
const prNumber = context.payload.issue.number;
|
||||||
console.log(`PR Number: ${prNumber}`);
|
console.log(`PR Number: ${prNumber}`);
|
||||||
core.setOutput('pr_number', prNumber);
|
core.setOutput('pr_number', prNumber);
|
||||||
|
|
||||||
- name: Get PR repository and ref
|
|
||||||
id: get-pr-info
|
|
||||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const { owner, repo } = context.repo;
|
|
||||||
const prNumber = context.payload.issue.number;
|
|
||||||
|
|
||||||
const { data: pr } = await github.rest.pulls.get({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
pull_number: prNumber,
|
|
||||||
});
|
|
||||||
|
|
||||||
// For forks, use the full repository name, for internal PRs use the current repo
|
|
||||||
const repository = pr.head.repo.fork ? pr.head.repo.full_name : `${owner}/${repo}`;
|
|
||||||
|
|
||||||
console.log(`PR Repository: ${repository}`);
|
|
||||||
console.log(`PR Branch: ${pr.head.ref}`);
|
|
||||||
|
|
||||||
core.setOutput('repository', repository);
|
|
||||||
core.setOutput('ref', pr.head.ref);
|
|
||||||
|
|
||||||
- name: Check for security/login flag
|
- name: Check for security/login flag
|
||||||
id: check-security-flag
|
id: check-security-flag
|
||||||
env:
|
env:
|
||||||
@@ -124,7 +99,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Add 'in_progress' reaction to comment
|
- name: Add 'in_progress' reaction to comment
|
||||||
id: add-eyes-reaction
|
id: add-eyes-reaction
|
||||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||||
with:
|
with:
|
||||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||||
script: |
|
script: |
|
||||||
@@ -153,12 +128,12 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Checkout PR
|
- name: Checkout PR
|
||||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Setup GitHub App Bot
|
- name: Setup GitHub App Bot
|
||||||
if: github.actor != 'dependabot[bot]'
|
if: github.actor != 'dependabot[bot]'
|
||||||
@@ -170,14 +145,13 @@ jobs:
|
|||||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
- name: Checkout PR
|
- name: Checkout PR
|
||||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
with:
|
with:
|
||||||
repository: ${{ needs.check-comment.outputs.pr_repository }}
|
ref: refs/pull/${{ needs.check-comment.outputs.pr_number }}/merge
|
||||||
ref: ${{ needs.check-comment.outputs.pr_ref }}
|
|
||||||
token: ${{ steps.setup-bot.outputs.token }}
|
token: ${{ steps.setup-bot.outputs.token }}
|
||||||
|
|
||||||
- name: Set up JDK
|
- name: Set up JDK
|
||||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||||
with:
|
with:
|
||||||
java-version: "17"
|
java-version: "17"
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
@@ -197,7 +171,7 @@ jobs:
|
|||||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_HUB_API }}
|
password: ${{ secrets.DOCKER_HUB_API }}
|
||||||
@@ -297,7 +271,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Add success reaction to comment
|
- name: Add success reaction to comment
|
||||||
if: success()
|
if: success()
|
||||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||||
with:
|
with:
|
||||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||||
script: |
|
script: |
|
||||||
@@ -332,7 +306,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Add failure reaction to comment
|
- name: Add failure reaction to comment
|
||||||
if: failure()
|
if: failure()
|
||||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||||
with:
|
with:
|
||||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||||
script: |
|
script: |
|
||||||
@@ -352,7 +326,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Post deployment URL to PR
|
- name: Post deployment URL to PR
|
||||||
if: success()
|
if: success()
|
||||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||||
with:
|
with:
|
||||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||||
script: |
|
script: |
|
||||||
|
|||||||
@@ -21,12 +21,12 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Checkout PR
|
- name: Checkout PR
|
||||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Setup GitHub App Bot
|
- name: Setup GitHub App Bot
|
||||||
if: github.actor != 'dependabot[bot]'
|
if: github.actor != 'dependabot[bot]'
|
||||||
@@ -39,7 +39,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Remove 'pr-deployed' label if present
|
- name: Remove 'pr-deployed' label if present
|
||||||
id: remove-label-comment
|
id: remove-label-comment
|
||||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||||
with:
|
with:
|
||||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||||
script: |
|
script: |
|
||||||
|
|||||||
@@ -19,11 +19,11 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -158,7 +158,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Post comment on PR if needed
|
- name: Post comment on PR if needed
|
||||||
if: steps.actor.outputs.is_repo_dev == 'true'
|
if: steps.actor.outputs.is_repo_dev == 'true'
|
||||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Setup GitHub App Bot
|
- name: Setup GitHub App Bot
|
||||||
id: setup-bot
|
id: setup-bot
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ jobs:
|
|||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Check for file changes
|
- name: Check for file changes
|
||||||
@@ -123,7 +122,8 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
java-version: "17"
|
java-version: "17"
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
- uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
- name: Setup Gradle
|
||||||
|
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||||
- name: Generate OpenAPI documentation
|
- name: Generate OpenAPI documentation
|
||||||
run: ./gradlew :stirling-pdf:generateOpenApiDocs
|
run: ./gradlew :stirling-pdf:generateOpenApiDocs
|
||||||
env:
|
env:
|
||||||
@@ -183,8 +183,10 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
java-version: "17"
|
java-version: "17"
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
|
|
||||||
- name: check the licenses for compatibility
|
- name: check the licenses for compatibility
|
||||||
run: ./gradlew clean checkLicense
|
run: ./gradlew clean checkLicense
|
||||||
|
|
||||||
- name: FAILED - check the licenses for compatibility
|
- name: FAILED - check the licenses for compatibility
|
||||||
if: failure()
|
if: failure()
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||||
|
|||||||
@@ -30,12 +30,12 @@ jobs:
|
|||||||
pull-requests: write # Allow writing to pull requests
|
pull-requests: write # Allow writing to pull requests
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Checkout main branch first
|
- name: Checkout main branch first
|
||||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Setup GitHub App Bot
|
- name: Setup GitHub App Bot
|
||||||
id: setup-bot
|
id: setup-bot
|
||||||
@@ -46,7 +46,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Get PR data
|
- name: Get PR data
|
||||||
id: get-pr-data
|
id: get-pr-data
|
||||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||||
with:
|
with:
|
||||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||||
script: |
|
script: |
|
||||||
@@ -90,7 +90,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Determine reference file test
|
- name: Determine reference file test
|
||||||
id: determine-file
|
id: determine-file
|
||||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||||
with:
|
with:
|
||||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||||
script: |
|
script: |
|
||||||
@@ -234,7 +234,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Post comment on PR
|
- name: Post comment on PR
|
||||||
if: env.SCRIPT_OUTPUT != ''
|
if: env.SCRIPT_OUTPUT != ''
|
||||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||||
with:
|
with:
|
||||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||||
script: |
|
script: |
|
||||||
|
|||||||
@@ -17,11 +17,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: "Checkout Repository"
|
- name: "Checkout Repository"
|
||||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: "Dependency Review"
|
- name: "Dependency Review"
|
||||||
uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d9 # v4.7.1
|
uses: actions/dependency-review-action@56339e523c0409420f6c2c9a2f4292bbb3c07dd3 # v4.8.0
|
||||||
|
|||||||
@@ -31,12 +31,12 @@ jobs:
|
|||||||
repository-projects: write # Required for enabling automerge
|
repository-projects: write # Required for enabling automerge
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -48,16 +48,19 @@ jobs:
|
|||||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
- name: Set up JDK 17
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||||
with:
|
with:
|
||||||
java-version: "17"
|
java-version: "17"
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
|
|
||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||||
|
|
||||||
- name: Check licenses for compatibility
|
- name: Check licenses for compatibility
|
||||||
run: ./gradlew clean checkLicense
|
run: ./gradlew clean checkLicense
|
||||||
|
env:
|
||||||
|
DISABLE_ADDITIONAL_FEATURES: false
|
||||||
|
STIRLING_PDF_DESKTOP_UI: true
|
||||||
|
|
||||||
- name: Upload artifact on failure
|
- name: Upload artifact on failure
|
||||||
if: failure()
|
if: failure()
|
||||||
|
|||||||
@@ -15,12 +15,12 @@ jobs:
|
|||||||
issues: write
|
issues: write
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Check out the repository
|
- name: Check out the repository
|
||||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Run Labeler
|
- name: Run Labeler
|
||||||
uses: crazy-max/ghaction-github-labeler@24d110aa46a59976b8a7f35518cb7f14f434c916 # v5.3.0
|
uses: crazy-max/ghaction-github-labeler@24d110aa46a59976b8a7f35518cb7f14f434c916 # v5.3.0
|
||||||
|
|||||||
@@ -21,14 +21,14 @@ jobs:
|
|||||||
versionMac: ${{ steps.versionNumberMac.outputs.versionNumberMac }}
|
versionMac: ${{ steps.versionNumberMac.outputs.versionNumberMac }}
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Set up JDK
|
- name: Set up JDK
|
||||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
@@ -60,19 +60,19 @@ jobs:
|
|||||||
file_suffix: ""
|
file_suffix: ""
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Set up JDK 21
|
- name: Set up JDK 21
|
||||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||||
with:
|
with:
|
||||||
java-version: "21"
|
java-version: "21"
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
|
|
||||||
- uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
- uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||||
with:
|
with:
|
||||||
gradle-version: 8.14
|
gradle-version: 8.14
|
||||||
|
|
||||||
@@ -110,7 +110,7 @@ jobs:
|
|||||||
file_suffix: ""
|
file_suffix: ""
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@@ -148,19 +148,19 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Set up JDK 21
|
- name: Set up JDK 21
|
||||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||||
with:
|
with:
|
||||||
java-version: "21"
|
java-version: "21"
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
|
|
||||||
- uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
- uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||||
with:
|
with:
|
||||||
gradle-version: 8.14
|
gradle-version: 8.14
|
||||||
|
|
||||||
@@ -238,7 +238,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@@ -252,7 +252,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install Cosign
|
- name: Install Cosign
|
||||||
if: matrix.os == 'windows-latest'
|
if: matrix.os == 'windows-latest'
|
||||||
uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2
|
uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0
|
||||||
|
|
||||||
- name: Generate key pair
|
- name: Generate key pair
|
||||||
if: matrix.os == 'windows-latest'
|
if: matrix.os == 'windows-latest'
|
||||||
@@ -301,7 +301,7 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@@ -310,7 +310,7 @@ jobs:
|
|||||||
- name: Display structure of downloaded files
|
- name: Display structure of downloaded files
|
||||||
run: ls -R
|
run: ls -R
|
||||||
- name: Upload binaries, attestations and signatures to Release and create GitHub Release
|
- name: Upload binaries, attestations and signatures to Release and create GitHub Release
|
||||||
uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2
|
uses: softprops/action-gh-release@62c96d0c4e8a889135c1f3a25910db8dbe0e85f7 # v2.3.4
|
||||||
with:
|
with:
|
||||||
tag_name: v${{ needs.read_versions.outputs.version }}
|
tag_name: v${{ needs.read_versions.outputs.version }}
|
||||||
generate_release_notes: true
|
generate_release_notes: true
|
||||||
|
|||||||
@@ -12,17 +12,21 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
pre-commit:
|
pre-commit:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
# Prevents sdist builds → no tar extraction
|
||||||
|
PIP_ONLY_BINARY: ":all:"
|
||||||
|
PIP_DISABLE_PIP_VERSION_CHECK: "1"
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -34,7 +38,7 @@ jobs:
|
|||||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||||
with:
|
with:
|
||||||
python-version: 3.12
|
python-version: 3.12
|
||||||
cache: 'pip' # caching pip dependencies
|
cache: 'pip' # caching pip dependencies
|
||||||
@@ -42,13 +46,13 @@ jobs:
|
|||||||
|
|
||||||
- name: Run Pre-Commit Hooks
|
- name: Run Pre-Commit Hooks
|
||||||
run: |
|
run: |
|
||||||
pip install --require-hashes -r ./.github/scripts/requirements_pre_commit.txt
|
pip install --require-hashes --only-binary=:all: -r ./.github/scripts/requirements_pre_commit.txt
|
||||||
|
|
||||||
- run: pre-commit run --all-files -c .pre-commit-config.yaml
|
- run: pre-commit run --all-files -c .pre-commit-config.yaml
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Set up JDK
|
- name: Set up JDK
|
||||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||||
with:
|
with:
|
||||||
java-version: 17
|
java-version: 17
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
|
|||||||
@@ -30,19 +30,19 @@ jobs:
|
|||||||
id-token: write
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Set up JDK 17
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||||
with:
|
with:
|
||||||
java-version: "17"
|
java-version: "17"
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
|
|
||||||
- uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
- uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||||
with:
|
with:
|
||||||
gradle-version: 8.14
|
gradle-version: 8.14
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install cosign
|
- name: Install cosign
|
||||||
if: github.ref == 'refs/heads/master'
|
if: github.ref == 'refs/heads/master'
|
||||||
uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2
|
uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0
|
||||||
with:
|
with:
|
||||||
cosign-release: "v2.4.1"
|
cosign-release: "v2.4.1"
|
||||||
|
|
||||||
@@ -67,13 +67,13 @@ jobs:
|
|||||||
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
|
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_HUB_API }}
|
password: ${{ secrets.DOCKER_HUB_API }}
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
|
|||||||
@@ -23,19 +23,19 @@ jobs:
|
|||||||
version: ${{ steps.versionNumber.outputs.versionNumber }}
|
version: ${{ steps.versionNumber.outputs.versionNumber }}
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Set up JDK 17
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||||
with:
|
with:
|
||||||
java-version: "17"
|
java-version: "17"
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
|
|
||||||
- uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
- uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||||
with:
|
with:
|
||||||
gradle-version: 8.14
|
gradle-version: 8.14
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ jobs:
|
|||||||
file_suffix: ""
|
file_suffix: ""
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ jobs:
|
|||||||
run: ls -R
|
run: ls -R
|
||||||
|
|
||||||
- name: Install Cosign
|
- name: Install Cosign
|
||||||
uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2
|
uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0
|
||||||
|
|
||||||
- name: Generate key pair
|
- name: Generate key pair
|
||||||
run: cosign generate-key-pair
|
run: cosign generate-key-pair
|
||||||
@@ -161,7 +161,7 @@ jobs:
|
|||||||
file_suffix: ""
|
file_suffix: ""
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@@ -171,7 +171,7 @@ jobs:
|
|||||||
name: signed${{ matrix.file_suffix }}
|
name: signed${{ matrix.file_suffix }}
|
||||||
|
|
||||||
- name: Upload binaries, attestations and signatures to Release and create GitHub Release
|
- name: Upload binaries, attestations and signatures to Release and create GitHub Release
|
||||||
uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2
|
uses: softprops/action-gh-release@62c96d0c4e8a889135c1f3a25910db8dbe0e85f7 # v2.3.4
|
||||||
with:
|
with:
|
||||||
tag_name: v${{ needs.build.outputs.version }}
|
tag_name: v${{ needs.build.outputs.version }}
|
||||||
generate_release_notes: true
|
generate_release_notes: true
|
||||||
|
|||||||
@@ -34,17 +34,17 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: "Checkout code"
|
- name: "Checkout code"
|
||||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: "Run analysis"
|
- name: "Run analysis"
|
||||||
uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2
|
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
|
||||||
with:
|
with:
|
||||||
results_file: results.sarif
|
results_file: results.sarif
|
||||||
results_format: sarif
|
results_format: sarif
|
||||||
@@ -74,6 +74,6 @@ jobs:
|
|||||||
|
|
||||||
# Upload the results to GitHub's code scanning dashboard.
|
# Upload the results to GitHub's code scanning dashboard.
|
||||||
- name: "Upload to code-scanning"
|
- name: "Upload to code-scanning"
|
||||||
uses: github/codeql-action/upload-sarif@76621b61decf072c1cee8dd1ce2d2a82d33c17ed # v3.29.5
|
uses: github/codeql-action/upload-sarif@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.29.5
|
||||||
with:
|
with:
|
||||||
sarif_file: results.sarif
|
sarif_file: results.sarif
|
||||||
|
|||||||
@@ -30,16 +30,16 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||||
|
|
||||||
- name: Build and analyze with Gradle
|
- name: Build and analyze with Gradle
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -16,12 +16,12 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: 30 days stale issues
|
- name: 30 days stale issues
|
||||||
uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0
|
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
days-before-stale: 30
|
days-before-stale: 30
|
||||||
|
|||||||
@@ -26,19 +26,19 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Set up JDK 17
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||||
with:
|
with:
|
||||||
java-version: "17"
|
java-version: "17"
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
|
|
||||||
- uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
- uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||||
|
|
||||||
- name: Generate Swagger documentation
|
- name: Generate Swagger documentation
|
||||||
run: ./gradlew :stirling-pdf:generateOpenApiDocs
|
run: ./gradlew :stirling-pdf:generateOpenApiDocs
|
||||||
|
|||||||
@@ -30,13 +30,17 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
sync-files:
|
sync-files:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
# Prevents sdist builds → no tar extraction
|
||||||
|
PIP_ONLY_BINARY: ":all:"
|
||||||
|
PIP_DISABLE_PIP_VERSION_CHECK: "1"
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Setup GitHub App Bot
|
- name: Setup GitHub App Bot
|
||||||
id: setup-bot
|
id: setup-bot
|
||||||
@@ -46,7 +50,7 @@ jobs:
|
|||||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.12"
|
||||||
cache: "pip" # caching pip dependencies
|
cache: "pip" # caching pip dependencies
|
||||||
@@ -61,7 +65,9 @@ jobs:
|
|||||||
git diff --staged --quiet || git commit -m ":memo: Sync translation files" || echo "No changes detected"
|
git diff --staged --quiet || git commit -m ":memo: Sync translation files" || echo "No changes detected"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pip install --require-hashes -r ./.github/scripts/requirements_sync_readme.txt
|
# Wheels-only + Hash-Pinning
|
||||||
|
run: |
|
||||||
|
pip install --require-hashes --only-binary=:all: -r ./.github/scripts/requirements_sync_readme.txt
|
||||||
|
|
||||||
- name: Sync README.md
|
- name: Sync README.md
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -24,21 +24,21 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Set up JDK
|
- name: Set up JDK
|
||||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||||
with:
|
with:
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
|
|
||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||||
with:
|
with:
|
||||||
gradle-version: 8.14
|
gradle-version: 8.14
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ jobs:
|
|||||||
echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT
|
echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_HUB_API }}
|
password: ${{ secrets.DOCKER_HUB_API }}
|
||||||
@@ -116,28 +116,47 @@ jobs:
|
|||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
files-changed:
|
||||||
|
if: always()
|
||||||
|
name: detect what files changed
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 3
|
||||||
|
outputs:
|
||||||
|
frontend: ${{ steps.changes.outputs.frontend }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
|
- name: Check for file changes
|
||||||
|
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||||
|
id: changes
|
||||||
|
with:
|
||||||
|
filters: ".github/config/.files.yaml"
|
||||||
|
|
||||||
test:
|
test:
|
||||||
needs: deploy
|
if: needs.files-changed.outputs.frontend == 'true'
|
||||||
|
needs: [deploy, files-changed]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||||
with:
|
with:
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
|
cache-dependency-path: frontend/package-lock.json
|
||||||
|
|
||||||
- name: Run TestDriver.ai
|
- name: Run TestDriver.ai
|
||||||
uses: testdriverai/action@f0d0f45fdd684db628baa843fe9313f3ca3a8aa8 #1.1.3
|
uses: testdriverai/action@f0d0f45fdd684db628baa843fe9313f3ca3a8aa8 #1.1.3
|
||||||
with:
|
with:
|
||||||
key: ${{secrets.TESTDRIVER_API_KEY}}
|
key: ${{secrets.TESTDRIVER_API_KEY}}
|
||||||
prerun: |
|
prerun: |
|
||||||
|
cd frontend
|
||||||
npm install
|
npm install
|
||||||
npm run build
|
npm run build
|
||||||
npm install dashcam-chrome --save
|
npm install dashcam-chrome --save
|
||||||
@@ -156,7 +175,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@@ -167,6 +186,7 @@ jobs:
|
|||||||
sudo chmod 600 ../private.key
|
sudo chmod 600 ../private.key
|
||||||
|
|
||||||
- name: Cleanup deployment
|
- name: Cleanup deployment
|
||||||
|
if: always()
|
||||||
run: |
|
run: |
|
||||||
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} << EOF
|
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} << EOF
|
||||||
cd /stirling/test-${{ github.sha }}
|
cd /stirling/test-${{ github.sha }}
|
||||||
@@ -174,3 +194,4 @@ jobs:
|
|||||||
cd /stirling
|
cd /stirling
|
||||||
rm -rf test-${{ github.sha }}
|
rm -rf test-${{ github.sha }}
|
||||||
EOF
|
EOF
|
||||||
|
continue-on-error: true # Ensure cleanup runs even if previous steps fail
|
||||||
|
|||||||
@@ -204,6 +204,9 @@ id_ed25519.pub
|
|||||||
# node_modules
|
# node_modules
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
|
# weasyPrint
|
||||||
|
**/LOCAL_APPDATA_FONTCONFIG_CACHE/**
|
||||||
|
|
||||||
# Translation temp files
|
# Translation temp files
|
||||||
*_compact.json
|
*_compact.json
|
||||||
*compact*.json
|
*compact*.json
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
The newly introduced feature enhances the application with robust database backup and import capabilities. This feature is designed to ensure data integrity and provide a straightforward way to manage database backups. Here's how it works:
|
The newly introduced feature enhances the application with robust database backup and import capabilities. This feature is designed to ensure data integrity and provide a straightforward way to manage database backups. Here's how it works:
|
||||||
|
|
||||||
1. Automatic Backup Creation
|
1. Automatic Backup Creation
|
||||||
- The system automatically creates a database backup every day at midnight. This ensures that there is always a recent backup available, minimizing the risk of data loss.
|
- The system automatically creates a database backup on a configurable schedule (default: daily at midnight via `system.databaseBackup.cron`). This ensures that there is always a recent backup available, minimizing the risk of data loss.
|
||||||
2. Manual Backup Export
|
2. Manual Backup Export
|
||||||
- Admin actions that modify the user database trigger a manual export of the database. This keeps the backup up-to-date with the latest changes and provides an extra layer of data security.
|
- Admin actions that modify the user database trigger a manual export of the database. This keeps the backup up-to-date with the latest changes and provides an extra layer of data security.
|
||||||
3. Importing Database Backups
|
3. Importing Database Backups
|
||||||
|
|||||||
@@ -167,6 +167,10 @@
|
|||||||
{
|
{
|
||||||
"moduleName": ".*",
|
"moduleName": ".*",
|
||||||
"moduleLicense": "The W3C License"
|
"moduleLicense": "The W3C License"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"moduleName": ".*",
|
||||||
|
"moduleLicense": "UnRar License"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,13 +33,14 @@ dependencies {
|
|||||||
api 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20240325.1'
|
api 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20240325.1'
|
||||||
api 'com.fathzer:javaluator:3.0.6'
|
api 'com.fathzer:javaluator:3.0.6'
|
||||||
api 'com.posthog.java:posthog:1.2.0'
|
api 'com.posthog.java:posthog:1.2.0'
|
||||||
api 'org.apache.commons:commons-lang3:3.18.0'
|
api 'org.apache.commons:commons-lang3:3.19.0'
|
||||||
api 'com.drewnoakes:metadata-extractor:2.19.0' // Image metadata extractor
|
api 'com.drewnoakes:metadata-extractor:2.19.0' // Image metadata extractor
|
||||||
api 'com.vladsch.flexmark:flexmark-html2md-converter:0.64.8'
|
api 'com.vladsch.flexmark:flexmark-html2md-converter:0.64.8'
|
||||||
api "org.apache.pdfbox:pdfbox:$pdfboxVersion"
|
api "org.apache.pdfbox:pdfbox:$pdfboxVersion"
|
||||||
|
api 'com.github.junrar:junrar:7.5.5' // RAR archive support for CBR files
|
||||||
api 'jakarta.servlet:jakarta.servlet-api:6.1.0'
|
api 'jakarta.servlet:jakarta.servlet-api:6.1.0'
|
||||||
api 'org.snakeyaml:snakeyaml-engine:2.10'
|
api 'org.snakeyaml:snakeyaml-engine:2.10'
|
||||||
api "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.9"
|
api "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.13"
|
||||||
api 'jakarta.mail:jakarta.mail-api:2.1.3'
|
api 'jakarta.mail:jakarta.mail-api:2.1.5'
|
||||||
runtimeOnly 'org.eclipse.angus:angus-mail:2.0.4'
|
runtimeOnly 'org.eclipse.angus:angus-mail:2.0.5'
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -3,6 +3,7 @@ package stirling.software.common.annotations;
|
|||||||
import java.lang.annotation.*;
|
import java.lang.annotation.*;
|
||||||
|
|
||||||
import org.springframework.core.annotation.AliasFor;
|
import org.springframework.core.annotation.AliasFor;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMethod;
|
import org.springframework.web.bind.annotation.RequestMethod;
|
||||||
|
|
||||||
@@ -40,7 +41,7 @@ public @interface AutoJobPostMapping {
|
|||||||
|
|
||||||
/** MIME types this endpoint accepts. Defaults to {@code multipart/form-data}. */
|
/** MIME types this endpoint accepts. Defaults to {@code multipart/form-data}. */
|
||||||
@AliasFor(annotation = RequestMapping.class, attribute = "consumes")
|
@AliasFor(annotation = RequestMapping.class, attribute = "consumes")
|
||||||
String[] consumes() default {"multipart/form-data"};
|
String[] consumes() default {MediaType.MULTIPART_FORM_DATA_VALUE};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maximum execution time in milliseconds before the job is aborted. A negative value means "use
|
* Maximum execution time in milliseconds before the job is aborted. A negative value means "use
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
|
|
||||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import stirling.software.common.model.api.PDFFile;
|
import stirling.software.common.model.api.PDFFile;
|
||||||
import stirling.software.common.service.FileOrUploadService;
|
|
||||||
import stirling.software.common.service.FileStorage;
|
import stirling.software.common.service.FileStorage;
|
||||||
import stirling.software.common.service.JobExecutorService;
|
import stirling.software.common.service.JobExecutorService;
|
||||||
|
|
||||||
@@ -34,7 +33,6 @@ public class AutoJobAspect {
|
|||||||
|
|
||||||
private final JobExecutorService jobExecutorService;
|
private final JobExecutorService jobExecutorService;
|
||||||
private final HttpServletRequest request;
|
private final HttpServletRequest request;
|
||||||
private final FileOrUploadService fileOrUploadService;
|
|
||||||
private final FileStorage fileStorage;
|
private final FileStorage fileStorage;
|
||||||
|
|
||||||
@Around("@annotation(autoJobPostMapping)")
|
@Around("@annotation(autoJobPostMapping)")
|
||||||
@@ -53,7 +51,8 @@ public class AutoJobAspect {
|
|||||||
boolean trackProgress = autoJobPostMapping.trackProgress();
|
boolean trackProgress = autoJobPostMapping.trackProgress();
|
||||||
|
|
||||||
log.debug(
|
log.debug(
|
||||||
"AutoJobPostMapping execution with async={}, timeout={}, retryCount={}, trackProgress={}",
|
"AutoJobPostMapping execution with async={}, timeout={}, retryCount={},"
|
||||||
|
+ " trackProgress={}",
|
||||||
async,
|
async,
|
||||||
timeout > 0 ? timeout : "default",
|
timeout > 0 ? timeout : "default",
|
||||||
retryCount,
|
retryCount,
|
||||||
@@ -148,7 +147,8 @@ public class AutoJobAspect {
|
|||||||
} catch (Throwable ex) {
|
} catch (Throwable ex) {
|
||||||
lastException = ex;
|
lastException = ex;
|
||||||
log.error(
|
log.error(
|
||||||
"AutoJobAspect caught exception during job execution (attempt {}/{}): {}",
|
"AutoJobAspect caught exception during job execution (attempt"
|
||||||
|
+ " {}/{}): {}",
|
||||||
currentAttempt,
|
currentAttempt,
|
||||||
maxRetries,
|
maxRetries,
|
||||||
ex.getMessage(),
|
ex.getMessage(),
|
||||||
|
|||||||
+21
-1
@@ -23,10 +23,30 @@ import stirling.software.common.util.YamlHelper;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class ConfigInitializer {
|
public class ConfigInitializer {
|
||||||
|
|
||||||
|
private static final int MIN_SETTINGS_FILE_LINES = 31;
|
||||||
|
|
||||||
public void ensureConfigExists() throws IOException, URISyntaxException {
|
public void ensureConfigExists() throws IOException, URISyntaxException {
|
||||||
// 1) If settings file doesn't exist, create from template
|
// 1) If settings file doesn't exist, create from template
|
||||||
Path destPath = Paths.get(InstallationPathConfig.getSettingsPath());
|
Path destPath = Paths.get(InstallationPathConfig.getSettingsPath());
|
||||||
if (Files.notExists(destPath)) {
|
|
||||||
|
boolean settingsFileExists = Files.exists(destPath);
|
||||||
|
|
||||||
|
long lineCount = settingsFileExists ? Files.readAllLines(destPath).size() : 0;
|
||||||
|
|
||||||
|
log.info("Current settings file line count: {}", lineCount);
|
||||||
|
|
||||||
|
if (!settingsFileExists || lineCount < MIN_SETTINGS_FILE_LINES) {
|
||||||
|
if (settingsFileExists) {
|
||||||
|
// move settings.yml to settings.yml.{timestamp}.bak
|
||||||
|
Path backupPath =
|
||||||
|
Paths.get(
|
||||||
|
InstallationPathConfig.getSettingsPath()
|
||||||
|
+ "."
|
||||||
|
+ System.currentTimeMillis()
|
||||||
|
+ ".bak");
|
||||||
|
Files.move(destPath, backupPath, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
log.info("Moved existing settings file to backup: {}", backupPath);
|
||||||
|
}
|
||||||
Files.createDirectories(destPath.getParent());
|
Files.createDirectories(destPath.getParent());
|
||||||
try (InputStream in =
|
try (InputStream in =
|
||||||
getClass().getClassLoader().getResourceAsStream("settings.yml.template")) {
|
getClass().getClassLoader().getResourceAsStream("settings.yml.template")) {
|
||||||
|
|||||||
+16
-2
@@ -14,12 +14,17 @@ public class InstallationPathConfig {
|
|||||||
private static final String CONFIG_PATH;
|
private static final String CONFIG_PATH;
|
||||||
private static final String CUSTOM_FILES_PATH;
|
private static final String CUSTOM_FILES_PATH;
|
||||||
private static final String CLIENT_WEBUI_PATH;
|
private static final String CLIENT_WEBUI_PATH;
|
||||||
private static final String SCRIPTS_PATH;
|
|
||||||
private static final String PIPELINE_PATH;
|
private static final String PIPELINE_PATH;
|
||||||
|
|
||||||
// Config paths
|
// Config paths
|
||||||
private static final String SETTINGS_PATH;
|
private static final String SETTINGS_PATH;
|
||||||
private static final String CUSTOM_SETTINGS_PATH;
|
private static final String CUSTOM_SETTINGS_PATH;
|
||||||
|
private static final String SCRIPTS_PATH;
|
||||||
|
private static final String BACKUP_PATH;
|
||||||
|
|
||||||
|
// Backup paths
|
||||||
|
private static final String BACKUP_DB_PATH;
|
||||||
|
private static final String BACKUP_PRIVATE_KEY_PATH;
|
||||||
|
|
||||||
// Custom file paths
|
// Custom file paths
|
||||||
private static final String STATIC_PATH;
|
private static final String STATIC_PATH;
|
||||||
@@ -41,6 +46,11 @@ public class InstallationPathConfig {
|
|||||||
SETTINGS_PATH = CONFIG_PATH + "settings.yml";
|
SETTINGS_PATH = CONFIG_PATH + "settings.yml";
|
||||||
CUSTOM_SETTINGS_PATH = CONFIG_PATH + "custom_settings.yml";
|
CUSTOM_SETTINGS_PATH = CONFIG_PATH + "custom_settings.yml";
|
||||||
SCRIPTS_PATH = CONFIG_PATH + "scripts" + File.separator;
|
SCRIPTS_PATH = CONFIG_PATH + "scripts" + File.separator;
|
||||||
|
BACKUP_PATH = CONFIG_PATH + "backup" + File.separator;
|
||||||
|
|
||||||
|
// Initialize backup paths
|
||||||
|
BACKUP_DB_PATH = BACKUP_PATH + "db" + File.separator;
|
||||||
|
BACKUP_PRIVATE_KEY_PATH = BACKUP_PATH + "keys" + File.separator;
|
||||||
|
|
||||||
// Initialize custom file paths
|
// Initialize custom file paths
|
||||||
STATIC_PATH = CUSTOM_FILES_PATH + "static" + File.separator;
|
STATIC_PATH = CUSTOM_FILES_PATH + "static" + File.separator;
|
||||||
@@ -124,6 +134,10 @@ public class InstallationPathConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static String getPrivateKeyPath() {
|
public static String getPrivateKeyPath() {
|
||||||
return PRIVATE_KEY_PATH;
|
return BACKUP_PRIVATE_KEY_PATH;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getBackupPath() {
|
||||||
|
return BACKUP_DB_PATH;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-14
@@ -41,6 +41,7 @@ import stirling.software.common.model.oauth2.GitHubProvider;
|
|||||||
import stirling.software.common.model.oauth2.GoogleProvider;
|
import stirling.software.common.model.oauth2.GoogleProvider;
|
||||||
import stirling.software.common.model.oauth2.KeycloakProvider;
|
import stirling.software.common.model.oauth2.KeycloakProvider;
|
||||||
import stirling.software.common.model.oauth2.Provider;
|
import stirling.software.common.model.oauth2.Provider;
|
||||||
|
import stirling.software.common.service.SsrfProtectionService.SsrfProtectionLevel;
|
||||||
import stirling.software.common.util.ValidationUtils;
|
import stirling.software.common.util.ValidationUtils;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@@ -72,11 +73,11 @@ public class ApplicationProperties {
|
|||||||
public PropertySource<?> dynamicYamlPropertySource(ConfigurableEnvironment environment)
|
public PropertySource<?> dynamicYamlPropertySource(ConfigurableEnvironment environment)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
String configPath = InstallationPathConfig.getSettingsPath();
|
String configPath = InstallationPathConfig.getSettingsPath();
|
||||||
log.debug("Attempting to load settings from: " + configPath);
|
log.debug("Attempting to load settings from: {}", configPath);
|
||||||
|
|
||||||
File file = new File(configPath);
|
File file = new File(configPath);
|
||||||
if (!file.exists()) {
|
if (!file.exists()) {
|
||||||
log.error("Warning: Settings file does not exist at: " + configPath);
|
log.error("Warning: Settings file does not exist at: {}", configPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
Resource resource = new FileSystemResource(configPath);
|
Resource resource = new FileSystemResource(configPath);
|
||||||
@@ -89,7 +90,7 @@ public class ApplicationProperties {
|
|||||||
new YamlPropertySourceFactory().createPropertySource(null, encodedResource);
|
new YamlPropertySourceFactory().createPropertySource(null, encodedResource);
|
||||||
environment.getPropertySources().addFirst(propertySource);
|
environment.getPropertySources().addFirst(propertySource);
|
||||||
|
|
||||||
log.debug("Loaded properties: " + propertySource.getSource());
|
log.debug("Loaded properties: {}", propertySource.getSource());
|
||||||
|
|
||||||
return propertySource;
|
return propertySource;
|
||||||
}
|
}
|
||||||
@@ -365,6 +366,7 @@ public class ApplicationProperties {
|
|||||||
private CustomPaths customPaths = new CustomPaths();
|
private CustomPaths customPaths = new CustomPaths();
|
||||||
private String fileUploadLimit;
|
private String fileUploadLimit;
|
||||||
private TempFileManagement tempFileManagement = new TempFileManagement();
|
private TempFileManagement tempFileManagement = new TempFileManagement();
|
||||||
|
private DatabaseBackup databaseBackup = new DatabaseBackup();
|
||||||
private List<String> corsAllowedOrigins = new ArrayList<>();
|
private List<String> corsAllowedOrigins = new ArrayList<>();
|
||||||
|
|
||||||
public boolean isAnalyticsEnabled() {
|
public boolean isAnalyticsEnabled() {
|
||||||
@@ -384,6 +386,11 @@ public class ApplicationProperties {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class DatabaseBackup {
|
||||||
|
private String cron = "0 0 0 * * ?"; // daily at midnight
|
||||||
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public static class CustomPaths {
|
public static class CustomPaths {
|
||||||
private Pipeline pipeline = new Pipeline();
|
private Pipeline pipeline = new Pipeline();
|
||||||
@@ -420,17 +427,19 @@ public class ApplicationProperties {
|
|||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
public String getBaseTmpDir() {
|
public String getBaseTmpDir() {
|
||||||
return baseTmpDir != null && !baseTmpDir.isEmpty()
|
if (baseTmpDir != null && !baseTmpDir.isEmpty()) {
|
||||||
? baseTmpDir
|
return baseTmpDir;
|
||||||
: java.lang.System.getProperty("java.io.tmpdir").replaceAll("/+$", "")
|
}
|
||||||
+ "/stirling-pdf";
|
String tmp = java.lang.System.getProperty("java.io.tmpdir");
|
||||||
|
return new File(tmp, "stirling-pdf").getPath();
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
public String getLibreofficeDir() {
|
public String getLibreofficeDir() {
|
||||||
return libreofficeDir != null && !libreofficeDir.isEmpty()
|
if (libreofficeDir != null && !libreofficeDir.isEmpty()) {
|
||||||
? libreofficeDir
|
return libreofficeDir;
|
||||||
: getBaseTmpDir() + "/libreoffice";
|
}
|
||||||
|
return new File(getBaseTmpDir(), "libreoffice").getPath();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -441,7 +450,7 @@ public class ApplicationProperties {
|
|||||||
@Data
|
@Data
|
||||||
public static class UrlSecurity {
|
public static class UrlSecurity {
|
||||||
private boolean enabled = true;
|
private boolean enabled = true;
|
||||||
private String level = "MEDIUM"; // MAX, MEDIUM, OFF
|
private SsrfProtectionLevel level = SsrfProtectionLevel.MEDIUM; // MAX, MEDIUM, OFF
|
||||||
private List<String> allowedDomains = new ArrayList<>();
|
private List<String> allowedDomains = new ArrayList<>();
|
||||||
private List<String> blockedDomains = new ArrayList<>();
|
private List<String> blockedDomains = new ArrayList<>();
|
||||||
private List<String> internalTlds =
|
private List<String> internalTlds =
|
||||||
@@ -494,9 +503,7 @@ public class ApplicationProperties {
|
|||||||
private List<String> languages;
|
private List<String> languages;
|
||||||
|
|
||||||
public String getAppNameNavbar() {
|
public String getAppNameNavbar() {
|
||||||
return appNameNavbar != null && appNameNavbar.trim().length() > 0
|
return appNameNavbar != null && !appNameNavbar.trim().isEmpty() ? appNameNavbar : null;
|
||||||
? appNameNavbar
|
|
||||||
: null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ import java.util.Locale;
|
|||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
@AllArgsConstructor
|
|
||||||
@Data
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
public class FileInfo {
|
public class FileInfo {
|
||||||
private static final DateTimeFormatter DATE_FORMATTER =
|
private static final DateTimeFormatter DATE_FORMATTER =
|
||||||
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||||
|
|||||||
+5
-7
@@ -4,14 +4,12 @@ package stirling.software.common.model;
|
|||||||
* This class will be removed when frontend migration to React is complete
|
* This class will be removed when frontend migration to React is complete
|
||||||
|
|
||||||
|
|
||||||
public class InputStreamTemplateResource implements ITemplateResource {
|
|
||||||
private InputStream inputStream;
|
|
||||||
private String characterEncoding;
|
|
||||||
|
|
||||||
public InputStreamTemplateResource(InputStream inputStream, String characterEncoding) {
|
@RequiredArgsConstructor
|
||||||
this.inputStream = inputStream;
|
@Getter
|
||||||
this.characterEncoding = characterEncoding;
|
public class InputStreamTemplateResource implements ITemplateResource {
|
||||||
}
|
private final InputStream inputStream;
|
||||||
|
private final String characterEncoding;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Reader reader() throws IOException {
|
public Reader reader() throws IOException {
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
package stirling.software.common.model;
|
package stirling.software.common.model;
|
||||||
|
|
||||||
import java.util.Calendar;
|
import java.time.ZonedDateTime;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@Builder
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
public class PdfMetadata {
|
public class PdfMetadata {
|
||||||
private String author;
|
private String author;
|
||||||
private String producer;
|
private String producer;
|
||||||
@@ -14,6 +18,6 @@ public class PdfMetadata {
|
|||||||
private String creator;
|
private String creator;
|
||||||
private String subject;
|
private String subject;
|
||||||
private String keywords;
|
private String keywords;
|
||||||
private Calendar creationDate;
|
private ZonedDateTime creationDate;
|
||||||
private Calendar modificationDate;
|
private ZonedDateTime modificationDate;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package stirling.software.common.model.api;
|
package stirling.software.common.model.api;
|
||||||
|
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
@@ -15,7 +16,11 @@ import lombok.NoArgsConstructor;
|
|||||||
@EqualsAndHashCode
|
@EqualsAndHashCode
|
||||||
@Schema(description = "PDF file input - either upload a file or provide a server-side file ID")
|
@Schema(description = "PDF file input - either upload a file or provide a server-side file ID")
|
||||||
public class PDFFile {
|
public class PDFFile {
|
||||||
@Schema(description = "The input PDF file", format = "binary")
|
|
||||||
|
@Schema(
|
||||||
|
description = "The input PDF file",
|
||||||
|
contentMediaType = MediaType.APPLICATION_PDF_VALUE,
|
||||||
|
format = "binary")
|
||||||
private MultipartFile fileInput;
|
private MultipartFile fileInput;
|
||||||
|
|
||||||
@Schema(
|
@Schema(
|
||||||
|
|||||||
@@ -4,4 +4,5 @@ public enum ReplaceAndInvert {
|
|||||||
HIGH_CONTRAST_COLOR,
|
HIGH_CONTRAST_COLOR,
|
||||||
CUSTOM_COLOR,
|
CUSTOM_COLOR,
|
||||||
FULL_INVERSION,
|
FULL_INVERSION,
|
||||||
|
COLOR_SPACE_CONVERSION,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
|
|
||||||
import stirling.software.common.model.job.JobResponse;
|
import stirling.software.common.model.job.JobResponse;
|
||||||
import stirling.software.common.util.ExecutorFactory;
|
import stirling.software.common.util.ExecutorFactory;
|
||||||
|
import stirling.software.common.util.RegexPatternUtils;
|
||||||
|
|
||||||
/** Service for executing jobs asynchronously or synchronously */
|
/** Service for executing jobs asynchronously or synchronously */
|
||||||
@Service
|
@Service
|
||||||
@@ -227,7 +228,8 @@ public class JobExecutorService {
|
|||||||
if (result instanceof byte[]) {
|
if (result instanceof byte[]) {
|
||||||
// Store byte array directly to disk to avoid double memory consumption
|
// Store byte array directly to disk to avoid double memory consumption
|
||||||
String fileId = fileStorage.storeBytes((byte[]) result, "result.pdf");
|
String fileId = fileStorage.storeBytes((byte[]) result, "result.pdf");
|
||||||
taskManager.setFileResult(jobId, fileId, "result.pdf", "application/pdf");
|
taskManager.setFileResult(
|
||||||
|
jobId, fileId, "result.pdf", MediaType.APPLICATION_PDF_VALUE);
|
||||||
log.debug("Stored byte[] result with fileId: {}", fileId);
|
log.debug("Stored byte[] result with fileId: {}", fileId);
|
||||||
|
|
||||||
// Let the byte array get collected naturally in the next GC cycle
|
// Let the byte array get collected naturally in the next GC cycle
|
||||||
@@ -239,7 +241,7 @@ public class JobExecutorService {
|
|||||||
if (body instanceof byte[]) {
|
if (body instanceof byte[]) {
|
||||||
// Extract filename from content-disposition header if available
|
// Extract filename from content-disposition header if available
|
||||||
String filename = "result.pdf";
|
String filename = "result.pdf";
|
||||||
String contentType = "application/pdf";
|
String contentType = MediaType.APPLICATION_PDF_VALUE;
|
||||||
|
|
||||||
if (response.getHeaders().getContentDisposition() != null) {
|
if (response.getHeaders().getContentDisposition() != null) {
|
||||||
String disposition =
|
String disposition =
|
||||||
@@ -252,8 +254,10 @@ public class JobExecutorService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.getHeaders().getContentType() != null) {
|
MediaType mediaType = response.getHeaders().getContentType();
|
||||||
contentType = response.getHeaders().getContentType().toString();
|
|
||||||
|
if (mediaType != null) {
|
||||||
|
contentType = mediaType.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store byte array directly to disk
|
// Store byte array directly to disk
|
||||||
@@ -274,7 +278,7 @@ public class JobExecutorService {
|
|||||||
if (fileId != null && !fileId.isEmpty()) {
|
if (fileId != null && !fileId.isEmpty()) {
|
||||||
// Try to get filename and content type
|
// Try to get filename and content type
|
||||||
String filename = "result.pdf";
|
String filename = "result.pdf";
|
||||||
String contentType = "application/pdf";
|
String contentType = MediaType.APPLICATION_PDF_VALUE;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
java.lang.reflect.Method getOriginalFileName =
|
java.lang.reflect.Method getOriginalFileName =
|
||||||
@@ -315,8 +319,7 @@ public class JobExecutorService {
|
|||||||
// Store generic result
|
// Store generic result
|
||||||
taskManager.setResult(jobId, body);
|
taskManager.setResult(jobId, body);
|
||||||
}
|
}
|
||||||
} else if (result instanceof MultipartFile) {
|
} else if (result instanceof MultipartFile file) {
|
||||||
MultipartFile file = (MultipartFile) result;
|
|
||||||
String fileId = fileStorage.storeFile(file);
|
String fileId = fileStorage.storeFile(file);
|
||||||
taskManager.setFileResult(
|
taskManager.setFileResult(
|
||||||
jobId, fileId, file.getOriginalFilename(), file.getContentType());
|
jobId, fileId, file.getOriginalFilename(), file.getContentType());
|
||||||
@@ -333,7 +336,7 @@ public class JobExecutorService {
|
|||||||
if (fileId != null && !fileId.isEmpty()) {
|
if (fileId != null && !fileId.isEmpty()) {
|
||||||
// Try to get filename and content type
|
// Try to get filename and content type
|
||||||
String filename = "result.pdf";
|
String filename = "result.pdf";
|
||||||
String contentType = "application/pdf";
|
String contentType = MediaType.APPLICATION_PDF_VALUE;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
java.lang.reflect.Method getOriginalFileName =
|
java.lang.reflect.Method getOriginalFileName =
|
||||||
@@ -396,9 +399,8 @@ public class JobExecutorService {
|
|||||||
HttpHeaders.CONTENT_DISPOSITION,
|
HttpHeaders.CONTENT_DISPOSITION,
|
||||||
"form-data; name=\"attachment\"; filename=\"result.pdf\"")
|
"form-data; name=\"attachment\"; filename=\"result.pdf\"")
|
||||||
.body(result);
|
.body(result);
|
||||||
} else if (result instanceof MultipartFile) {
|
} else if (result instanceof MultipartFile file) {
|
||||||
// Return MultipartFile content
|
// Return MultipartFile content
|
||||||
MultipartFile file = (MultipartFile) result;
|
|
||||||
return ResponseEntity.ok()
|
return ResponseEntity.ok()
|
||||||
.contentType(MediaType.parseMediaType(file.getContentType()))
|
.contentType(MediaType.parseMediaType(file.getContentType()))
|
||||||
.header(
|
.header(
|
||||||
@@ -425,8 +427,16 @@ public class JobExecutorService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
String value = timeout.replaceAll("[^\\d.]", "");
|
String value =
|
||||||
String unit = timeout.replaceAll("[\\d.]", "");
|
RegexPatternUtils.getInstance()
|
||||||
|
.getNonDigitDotPattern()
|
||||||
|
.matcher(timeout)
|
||||||
|
.replaceAll("");
|
||||||
|
String unit =
|
||||||
|
RegexPatternUtils.getInstance()
|
||||||
|
.getDigitDotPattern()
|
||||||
|
.matcher(timeout)
|
||||||
|
.replaceAll("");
|
||||||
|
|
||||||
double numericValue = Double.parseDouble(value);
|
double numericValue = Double.parseDouble(value);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
package stirling.software.common.service;
|
package stirling.software.common.service;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
|
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
@@ -29,17 +33,19 @@ public class PdfMetadataService {
|
|||||||
this.runningProOrHigher = runningProOrHigher;
|
this.runningProOrHigher = runningProOrHigher;
|
||||||
}
|
}
|
||||||
|
|
||||||
public PdfMetadata extractMetadataFromPdf(PDDocument pdf) {
|
/**
|
||||||
return PdfMetadata.builder()
|
* Converts ZonedDateTime to Calendar for PDFBox compatibility.
|
||||||
.author(pdf.getDocumentInformation().getAuthor())
|
*
|
||||||
.producer(pdf.getDocumentInformation().getProducer())
|
* @param zonedDateTime the ZonedDateTime to convert
|
||||||
.title(pdf.getDocumentInformation().getTitle())
|
* @return Calendar instance or null if input is null
|
||||||
.creator(pdf.getDocumentInformation().getCreator())
|
*/
|
||||||
.subject(pdf.getDocumentInformation().getSubject())
|
public static Calendar toCalendar(ZonedDateTime zonedDateTime) {
|
||||||
.keywords(pdf.getDocumentInformation().getKeywords())
|
if (zonedDateTime == null) {
|
||||||
.creationDate(pdf.getDocumentInformation().getCreationDate())
|
return null;
|
||||||
.modificationDate(pdf.getDocumentInformation().getModificationDate())
|
}
|
||||||
.build();
|
Calendar calendar = Calendar.getInstance();
|
||||||
|
calendar.setTimeInMillis(zonedDateTime.toInstant().toEpochMilli());
|
||||||
|
return calendar;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDefaultMetadata(PDDocument pdf) {
|
public void setDefaultMetadata(PDDocument pdf) {
|
||||||
@@ -58,6 +64,52 @@ public class PdfMetadataService {
|
|||||||
setCommonMetadata(pdf, pdfMetadata);
|
setCommonMetadata(pdf, pdfMetadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a date string and converts it to Calendar for PDFBox compatibility.
|
||||||
|
*
|
||||||
|
* @param dateString the date string in "yyyy/MM/dd HH:mm:ss" format
|
||||||
|
* @return Calendar instance or null if parsing fails or input is empty
|
||||||
|
*/
|
||||||
|
public static Calendar parseToCalendar(String dateString) {
|
||||||
|
if (dateString == null || dateString.trim().isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
|
||||||
|
ZonedDateTime zonedDateTime =
|
||||||
|
LocalDateTime.parse(dateString, formatter).atZone(ZoneId.systemDefault());
|
||||||
|
return toCalendar(zonedDateTime);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public PdfMetadata extractMetadataFromPdf(PDDocument pdf) {
|
||||||
|
Calendar creationCal = pdf.getDocumentInformation().getCreationDate();
|
||||||
|
Calendar modificationCal = pdf.getDocumentInformation().getModificationDate();
|
||||||
|
|
||||||
|
ZonedDateTime creationDate =
|
||||||
|
creationCal != null
|
||||||
|
? ZonedDateTime.ofInstant(creationCal.toInstant(), ZoneId.systemDefault())
|
||||||
|
: null;
|
||||||
|
ZonedDateTime modificationDate =
|
||||||
|
modificationCal != null
|
||||||
|
? ZonedDateTime.ofInstant(
|
||||||
|
modificationCal.toInstant(), ZoneId.systemDefault())
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return PdfMetadata.builder()
|
||||||
|
.author(pdf.getDocumentInformation().getAuthor())
|
||||||
|
.producer(pdf.getDocumentInformation().getProducer())
|
||||||
|
.title(pdf.getDocumentInformation().getTitle())
|
||||||
|
.creator(pdf.getDocumentInformation().getCreator())
|
||||||
|
.subject(pdf.getDocumentInformation().getSubject())
|
||||||
|
.keywords(pdf.getDocumentInformation().getKeywords())
|
||||||
|
.creationDate(creationDate)
|
||||||
|
.modificationDate(modificationDate)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
private void setNewDocumentMetadata(PDDocument pdf, PdfMetadata pdfMetadata) {
|
private void setNewDocumentMetadata(PDDocument pdf, PdfMetadata pdfMetadata) {
|
||||||
|
|
||||||
String creator = stirlingPDFLabel;
|
String creator = stirlingPDFLabel;
|
||||||
@@ -79,7 +131,13 @@ public class PdfMetadataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pdf.getDocumentInformation().setCreator(creator);
|
pdf.getDocumentInformation().setCreator(creator);
|
||||||
pdf.getDocumentInformation().setCreationDate(Calendar.getInstance());
|
|
||||||
|
// Use existing creation date if available, otherwise create new one
|
||||||
|
Calendar creationCal =
|
||||||
|
pdfMetadata.getCreationDate() != null
|
||||||
|
? toCalendar(pdfMetadata.getCreationDate())
|
||||||
|
: Calendar.getInstance();
|
||||||
|
pdf.getDocumentInformation().setCreationDate(creationCal);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setCommonMetadata(PDDocument pdf, PdfMetadata pdfMetadata) {
|
private void setCommonMetadata(PDDocument pdf, PdfMetadata pdfMetadata) {
|
||||||
@@ -88,7 +146,13 @@ public class PdfMetadataService {
|
|||||||
pdf.getDocumentInformation().setProducer(stirlingPDFLabel);
|
pdf.getDocumentInformation().setProducer(stirlingPDFLabel);
|
||||||
pdf.getDocumentInformation().setSubject(pdfMetadata.getSubject());
|
pdf.getDocumentInformation().setSubject(pdfMetadata.getSubject());
|
||||||
pdf.getDocumentInformation().setKeywords(pdfMetadata.getKeywords());
|
pdf.getDocumentInformation().setKeywords(pdfMetadata.getKeywords());
|
||||||
pdf.getDocumentInformation().setModificationDate(Calendar.getInstance());
|
|
||||||
|
// Convert ZonedDateTime to Calendar for PDFBox compatibility
|
||||||
|
Calendar modificationCal =
|
||||||
|
pdfMetadata.getModificationDate() != null
|
||||||
|
? toCalendar(pdfMetadata.getModificationDate())
|
||||||
|
: Calendar.getInstance();
|
||||||
|
pdf.getDocumentInformation().setModificationDate(modificationCal);
|
||||||
|
|
||||||
String author = pdfMetadata.getAuthor();
|
String author = pdfMetadata.getAuthor();
|
||||||
if (applicationProperties
|
if (applicationProperties
|
||||||
|
|||||||
+83
-22
@@ -1,5 +1,7 @@
|
|||||||
package stirling.software.common.service;
|
package stirling.software.common.service;
|
||||||
|
|
||||||
|
import java.net.Inet4Address;
|
||||||
|
import java.net.Inet6Address;
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.UnknownHostException;
|
import java.net.UnknownHostException;
|
||||||
@@ -11,6 +13,7 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.common.model.ApplicationProperties;
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
|
import stirling.software.common.util.RegexPatternUtils;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@@ -20,8 +23,9 @@ public class SsrfProtectionService {
|
|||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
private static final Pattern DATA_URL_PATTERN =
|
private static final Pattern DATA_URL_PATTERN =
|
||||||
Pattern.compile("^data:.*", Pattern.CASE_INSENSITIVE);
|
RegexPatternUtils.getInstance().getPattern("^data:.*", Pattern.CASE_INSENSITIVE);
|
||||||
private static final Pattern FRAGMENT_PATTERN = Pattern.compile("^#.*");
|
private static final Pattern FRAGMENT_PATTERN =
|
||||||
|
RegexPatternUtils.getInstance().getPattern("^#.*");
|
||||||
|
|
||||||
public enum SsrfProtectionLevel {
|
public enum SsrfProtectionLevel {
|
||||||
OFF, // No SSRF protection - allows all URLs
|
OFF, // No SSRF protection - allows all URLs
|
||||||
@@ -51,21 +55,17 @@ public class SsrfProtectionService {
|
|||||||
|
|
||||||
SsrfProtectionLevel level = parseProtectionLevel(config.getLevel());
|
SsrfProtectionLevel level = parseProtectionLevel(config.getLevel());
|
||||||
|
|
||||||
switch (level) {
|
return switch (level) {
|
||||||
case OFF:
|
case OFF -> true;
|
||||||
return true;
|
case MAX -> isMaxSecurityAllowed(trimmedUrl, config);
|
||||||
case MAX:
|
case MEDIUM -> isMediumSecurityAllowed(trimmedUrl, config);
|
||||||
return isMaxSecurityAllowed(trimmedUrl, config);
|
default -> false;
|
||||||
case MEDIUM:
|
};
|
||||||
return isMediumSecurityAllowed(trimmedUrl, config);
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private SsrfProtectionLevel parseProtectionLevel(String level) {
|
private SsrfProtectionLevel parseProtectionLevel(SsrfProtectionLevel level) {
|
||||||
try {
|
try {
|
||||||
return SsrfProtectionLevel.valueOf(level.toUpperCase());
|
return SsrfProtectionLevel.valueOf(level.name());
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
log.warn("Invalid SSRF protection level '{}', defaulting to MEDIUM", level);
|
log.warn("Invalid SSRF protection level '{}', defaulting to MEDIUM", level);
|
||||||
return SsrfProtectionLevel.MEDIUM;
|
return SsrfProtectionLevel.MEDIUM;
|
||||||
@@ -172,15 +172,62 @@ public class SsrfProtectionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean isPrivateAddress(InetAddress address) {
|
private boolean isPrivateAddress(InetAddress address) {
|
||||||
return address.isSiteLocalAddress()
|
if (address.isAnyLocalAddress() || address.isLoopbackAddress()) {
|
||||||
|| address.isAnyLocalAddress()
|
return true;
|
||||||
|| isPrivateIPv4Range(address.getHostAddress());
|
}
|
||||||
|
|
||||||
|
if (address instanceof Inet4Address) {
|
||||||
|
return isPrivateIPv4Range(address.getHostAddress());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (address instanceof Inet6Address addr6) {
|
||||||
|
if (addr6.isLinkLocalAddress() || addr6.isSiteLocalAddress()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] bytes = addr6.getAddress();
|
||||||
|
if (isIpv4MappedAddress(bytes)) {
|
||||||
|
String ipv4 =
|
||||||
|
(bytes[12] & 0xff)
|
||||||
|
+ "."
|
||||||
|
+ (bytes[13] & 0xff)
|
||||||
|
+ "."
|
||||||
|
+ (bytes[14] & 0xff)
|
||||||
|
+ "."
|
||||||
|
+ (bytes[15] & 0xff);
|
||||||
|
return isPrivateIPv4Range(ipv4);
|
||||||
|
}
|
||||||
|
|
||||||
|
int firstByte = bytes[0] & 0xff;
|
||||||
|
// Check for IPv6 unique local addresses (fc00::/7)
|
||||||
|
if ((firstByte & 0xfe) == 0xfc) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isIpv4MappedAddress(byte[] addr) {
|
||||||
|
if (addr.length != 16) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
if (addr[i] != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// For IPv4-mapped IPv6 addresses, bytes 10 and 11 must be 0xff (i.e., address is
|
||||||
|
// ::ffff:w.x.y.z)
|
||||||
|
return addr[10] == (byte) 0xff && addr[11] == (byte) 0xff;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isPrivateIPv4Range(String ip) {
|
private boolean isPrivateIPv4Range(String ip) {
|
||||||
|
// Includes RFC1918, loopback, link-local, and unspecified addresses
|
||||||
return ip.startsWith("10.")
|
return ip.startsWith("10.")
|
||||||
|| ip.startsWith("192.168.")
|
|| ip.startsWith("192.168.")
|
||||||
|| (ip.startsWith("172.") && isInRange172(ip))
|
|| (ip.startsWith("172.") && isInRange172(ip))
|
||||||
|
|| ip.startsWith("169.254.")
|
||||||
|| ip.startsWith("127.")
|
|| ip.startsWith("127.")
|
||||||
|| "0.0.0.0".equals(ip);
|
|| "0.0.0.0".equals(ip);
|
||||||
}
|
}
|
||||||
@@ -192,17 +239,31 @@ public class SsrfProtectionService {
|
|||||||
int secondOctet = Integer.parseInt(parts[1]);
|
int secondOctet = Integer.parseInt(parts[1]);
|
||||||
return secondOctet >= 16 && secondOctet <= 31;
|
return secondOctet >= 16 && secondOctet <= 31;
|
||||||
} catch (NumberFormatException e) {
|
} catch (NumberFormatException e) {
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isCloudMetadataAddress(String ip) {
|
private boolean isCloudMetadataAddress(String ip) {
|
||||||
|
String normalizedIp = normalizeIpv4MappedAddress(ip);
|
||||||
// Cloud metadata endpoints for AWS, GCP, Azure, Oracle Cloud, and IBM Cloud
|
// Cloud metadata endpoints for AWS, GCP, Azure, Oracle Cloud, and IBM Cloud
|
||||||
return ip.startsWith("169.254.169.254") // AWS/GCP/Azure
|
return normalizedIp.startsWith("169.254.169.254") // AWS/GCP/Azure
|
||||||
|| ip.startsWith("fd00:ec2::254") // AWS IPv6
|
|| normalizedIp.startsWith("fd00:ec2::254") // AWS IPv6
|
||||||
|| ip.startsWith("169.254.169.253") // Oracle Cloud
|
|| normalizedIp.startsWith("169.254.169.253") // Oracle Cloud
|
||||||
|| ip.startsWith("169.254.169.250"); // IBM Cloud
|
|| normalizedIp.startsWith("169.254.169.250"); // IBM Cloud
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeIpv4MappedAddress(String ip) {
|
||||||
|
if (ip == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (ip.startsWith("::ffff:")) {
|
||||||
|
return ip.substring(7);
|
||||||
|
}
|
||||||
|
int lastColon = ip.lastIndexOf(':');
|
||||||
|
if (lastColon >= 0 && ip.indexOf('.') > lastColon) {
|
||||||
|
return ip.substring(lastColon + 1);
|
||||||
|
}
|
||||||
|
return ip;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-2
@@ -23,6 +23,7 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
|
|
||||||
import stirling.software.common.model.ApplicationProperties;
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
import stirling.software.common.util.GeneralUtils;
|
import stirling.software.common.util.GeneralUtils;
|
||||||
|
import stirling.software.common.util.RegexPatternUtils;
|
||||||
import stirling.software.common.util.TempFileManager;
|
import stirling.software.common.util.TempFileManager;
|
||||||
import stirling.software.common.util.TempFileRegistry;
|
import stirling.software.common.util.TempFileRegistry;
|
||||||
|
|
||||||
@@ -61,8 +62,14 @@ public class TempFileCleanupService {
|
|||||||
// File patterns that identify common system temp files
|
// File patterns that identify common system temp files
|
||||||
private static final Predicate<String> IS_SYSTEM_TEMP_FILE =
|
private static final Predicate<String> IS_SYSTEM_TEMP_FILE =
|
||||||
fileName ->
|
fileName ->
|
||||||
fileName.matches("lu\\d+[a-z0-9]*\\.tmp")
|
RegexPatternUtils.getInstance()
|
||||||
|| fileName.matches("ocr_process\\d+")
|
.getSystemTempFile1Pattern()
|
||||||
|
.matcher(fileName)
|
||||||
|
.matches()
|
||||||
|
|| RegexPatternUtils.getInstance()
|
||||||
|
.getSystemTempFile2Pattern()
|
||||||
|
.matcher(fileName)
|
||||||
|
.matches()
|
||||||
|| (fileName.startsWith("tmp") && !fileName.contains("jetty"))
|
|| (fileName.startsWith("tmp") && !fileName.contains("jetty"))
|
||||||
|| fileName.startsWith("OSL_PIPE_")
|
|| fileName.startsWith("OSL_PIPE_")
|
||||||
|| (fileName.endsWith(".tmp") && !fileName.contains("jetty"));
|
|| (fileName.endsWith(".tmp") && !fileName.contains("jetty"));
|
||||||
|
|||||||
@@ -0,0 +1,258 @@
|
|||||||
|
package stirling.software.common.util;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.apache.commons.io.FilenameUtils;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||||
|
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||||
|
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import com.github.junrar.Archive;
|
||||||
|
import com.github.junrar.exception.CorruptHeaderException;
|
||||||
|
import com.github.junrar.exception.RarException;
|
||||||
|
import com.github.junrar.rarfile.FileHeader;
|
||||||
|
|
||||||
|
import lombok.experimental.UtilityClass;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@UtilityClass
|
||||||
|
public class CbrUtils {
|
||||||
|
|
||||||
|
public byte[] convertCbrToPdf(
|
||||||
|
MultipartFile cbrFile,
|
||||||
|
CustomPDFDocumentFactory pdfDocumentFactory,
|
||||||
|
TempFileManager tempFileManager)
|
||||||
|
throws IOException {
|
||||||
|
return convertCbrToPdf(cbrFile, pdfDocumentFactory, tempFileManager, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] convertCbrToPdf(
|
||||||
|
MultipartFile cbrFile,
|
||||||
|
CustomPDFDocumentFactory pdfDocumentFactory,
|
||||||
|
TempFileManager tempFileManager,
|
||||||
|
boolean optimizeForEbook)
|
||||||
|
throws IOException {
|
||||||
|
|
||||||
|
validateCbrFile(cbrFile);
|
||||||
|
|
||||||
|
try (TempFile tempFile = new TempFile(tempFileManager, ".cbr")) {
|
||||||
|
cbrFile.transferTo(tempFile.getFile());
|
||||||
|
|
||||||
|
try (PDDocument document = pdfDocumentFactory.createNewDocument()) {
|
||||||
|
|
||||||
|
Archive archive;
|
||||||
|
try {
|
||||||
|
archive = new Archive(tempFile.getFile());
|
||||||
|
} catch (CorruptHeaderException e) {
|
||||||
|
log.warn(
|
||||||
|
"Failed to open CBR/RAR archive due to corrupt header: {}",
|
||||||
|
e.getMessage());
|
||||||
|
throw ExceptionUtils.createIllegalArgumentException(
|
||||||
|
"error.invalidFormat",
|
||||||
|
"Invalid or corrupted CBR/RAR archive. "
|
||||||
|
+ "The file may be corrupted, use an unsupported RAR format (RAR5+), "
|
||||||
|
+ "or may not be a valid RAR archive. "
|
||||||
|
+ "Please ensure the file is a valid RAR archive.");
|
||||||
|
} catch (RarException e) {
|
||||||
|
log.warn("Failed to open CBR/RAR archive: {}", e.getMessage());
|
||||||
|
String errorMessage;
|
||||||
|
String exMessage = e.getMessage() != null ? e.getMessage() : "";
|
||||||
|
|
||||||
|
if (exMessage.contains("encrypted")) {
|
||||||
|
errorMessage = "Encrypted CBR/RAR archives are not supported.";
|
||||||
|
} else if (exMessage.isEmpty()) {
|
||||||
|
errorMessage =
|
||||||
|
"Invalid CBR/RAR archive. "
|
||||||
|
+ "The file may be encrypted, corrupted, or use an unsupported format.";
|
||||||
|
} else {
|
||||||
|
errorMessage =
|
||||||
|
"Invalid CBR/RAR archive: "
|
||||||
|
+ exMessage
|
||||||
|
+ ". The file may be encrypted, corrupted, or use an unsupported format.";
|
||||||
|
}
|
||||||
|
throw ExceptionUtils.createIllegalArgumentException(
|
||||||
|
"error.invalidFormat", errorMessage);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("IO error reading CBR/RAR archive: {}", e.getMessage());
|
||||||
|
throw ExceptionUtils.createFileProcessingException("CBR extraction", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ImageEntryData> imageEntries = new ArrayList<>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (FileHeader fileHeader : archive) {
|
||||||
|
if (!fileHeader.isDirectory() && isImageFile(fileHeader.getFileName())) {
|
||||||
|
try (InputStream is = archive.getInputStream(fileHeader)) {
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
is.transferTo(baos);
|
||||||
|
imageEntries.add(
|
||||||
|
new ImageEntryData(
|
||||||
|
fileHeader.getFileName(), baos.toByteArray()));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn(
|
||||||
|
"Error reading image {}: {}",
|
||||||
|
fileHeader.getFileName(),
|
||||||
|
e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
archive.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("Error closing CBR/RAR archive: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
imageEntries.sort(
|
||||||
|
Comparator.comparing(ImageEntryData::name, new NaturalOrderComparator()));
|
||||||
|
|
||||||
|
if (imageEntries.isEmpty()) {
|
||||||
|
throw ExceptionUtils.createIllegalArgumentException(
|
||||||
|
"error.fileProcessing",
|
||||||
|
"No valid images found in the CBR file. The archive may be empty or contain no supported image formats.");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (ImageEntryData imageEntry : imageEntries) {
|
||||||
|
try {
|
||||||
|
PDImageXObject pdImage =
|
||||||
|
PDImageXObject.createFromByteArray(
|
||||||
|
document, imageEntry.data(), imageEntry.name());
|
||||||
|
PDPage page =
|
||||||
|
new PDPage(
|
||||||
|
new PDRectangle(pdImage.getWidth(), pdImage.getHeight()));
|
||||||
|
document.addPage(page);
|
||||||
|
try (PDPageContentStream contentStream =
|
||||||
|
new PDPageContentStream(document, page)) {
|
||||||
|
contentStream.drawImage(pdImage, 0, 0);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn(
|
||||||
|
"Error processing image {}: {}", imageEntry.name(), e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.getNumberOfPages() == 0) {
|
||||||
|
throw ExceptionUtils.createIllegalArgumentException(
|
||||||
|
"error.fileProcessing",
|
||||||
|
"No images could be processed from the CBR file. All images may be corrupted or in unsupported formats.");
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
document.save(baos);
|
||||||
|
byte[] pdfBytes = baos.toByteArray();
|
||||||
|
|
||||||
|
// Apply Ghostscript optimization if requested
|
||||||
|
if (optimizeForEbook) {
|
||||||
|
try {
|
||||||
|
return GeneralUtils.optimizePdfWithGhostscript(pdfBytes);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("Ghostscript optimization failed, returning unoptimized PDF", e);
|
||||||
|
return pdfBytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pdfBytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateCbrFile(MultipartFile file) {
|
||||||
|
if (file == null || file.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("File cannot be null or empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
String filename = file.getOriginalFilename();
|
||||||
|
if (filename == null) {
|
||||||
|
throw new IllegalArgumentException("File must have a name");
|
||||||
|
}
|
||||||
|
|
||||||
|
String extension = FilenameUtils.getExtension(filename).toLowerCase();
|
||||||
|
if (!"cbr".equals(extension) && !"rar".equals(extension)) {
|
||||||
|
throw new IllegalArgumentException("File must be a CBR or RAR archive");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isCbrFile(MultipartFile file) {
|
||||||
|
String filename = file.getOriginalFilename();
|
||||||
|
if (filename == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String extension = FilenameUtils.getExtension(filename).toLowerCase();
|
||||||
|
return "cbr".equals(extension) || "rar".equals(extension);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isImageFile(String filename) {
|
||||||
|
return RegexPatternUtils.getInstance().getImageFilePattern().matcher(filename).matches();
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ImageEntryData(String name, byte[] data) {}
|
||||||
|
|
||||||
|
private class NaturalOrderComparator implements Comparator<String> {
|
||||||
|
private static String getChunk(String s, int length, int marker) {
|
||||||
|
StringBuilder chunk = new StringBuilder();
|
||||||
|
char c = s.charAt(marker);
|
||||||
|
chunk.append(c);
|
||||||
|
marker++;
|
||||||
|
|
||||||
|
if (isDigit(c)) {
|
||||||
|
while (marker < length && isDigit(s.charAt(marker))) {
|
||||||
|
chunk.append(s.charAt(marker));
|
||||||
|
marker++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
while (marker < length && !isDigit(s.charAt(marker))) {
|
||||||
|
chunk.append(s.charAt(marker));
|
||||||
|
marker++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return chunk.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isDigit(char ch) {
|
||||||
|
return ch >= '0' && ch <= '9';
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compare(String s1, String s2) {
|
||||||
|
int len1 = s1.length();
|
||||||
|
int len2 = s2.length();
|
||||||
|
int marker1 = 0, marker2 = 0;
|
||||||
|
|
||||||
|
while (marker1 < len1 && marker2 < len2) {
|
||||||
|
String chunk1 = getChunk(s1, len1, marker1);
|
||||||
|
marker1 += chunk1.length();
|
||||||
|
|
||||||
|
String chunk2 = getChunk(s2, len2, marker2);
|
||||||
|
marker2 += chunk2.length();
|
||||||
|
|
||||||
|
int result;
|
||||||
|
if (isDigit(chunk1.charAt(0)) && isDigit(chunk2.charAt(0))) {
|
||||||
|
int thisNumericValue = Integer.parseInt(chunk1);
|
||||||
|
int thatNumericValue = Integer.parseInt(chunk2);
|
||||||
|
result = Integer.compare(thisNumericValue, thatNumericValue);
|
||||||
|
} else {
|
||||||
|
result = chunk1.compareTo(chunk2);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result != 0) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Integer.compare(len1, len2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
package stirling.software.common.util;
|
||||||
|
|
||||||
|
import java.io.BufferedInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipFile;
|
||||||
|
import java.util.zip.ZipInputStream;
|
||||||
|
|
||||||
|
import org.apache.commons.io.FilenameUtils;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||||
|
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||||
|
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import lombok.experimental.UtilityClass;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@UtilityClass
|
||||||
|
public class CbzUtils {
|
||||||
|
|
||||||
|
public byte[] convertCbzToPdf(
|
||||||
|
MultipartFile cbzFile,
|
||||||
|
CustomPDFDocumentFactory pdfDocumentFactory,
|
||||||
|
TempFileManager tempFileManager)
|
||||||
|
throws IOException {
|
||||||
|
return convertCbzToPdf(cbzFile, pdfDocumentFactory, tempFileManager, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] convertCbzToPdf(
|
||||||
|
MultipartFile cbzFile,
|
||||||
|
CustomPDFDocumentFactory pdfDocumentFactory,
|
||||||
|
TempFileManager tempFileManager,
|
||||||
|
boolean optimizeForEbook)
|
||||||
|
throws IOException {
|
||||||
|
|
||||||
|
validateCbzFile(cbzFile);
|
||||||
|
|
||||||
|
try (TempFile tempFile = new TempFile(tempFileManager, ".cbz")) {
|
||||||
|
cbzFile.transferTo(tempFile.getFile());
|
||||||
|
|
||||||
|
// Early ZIP validity check using ZipInputStream (fail fast on non-zip content)
|
||||||
|
try (BufferedInputStream bis =
|
||||||
|
new BufferedInputStream(
|
||||||
|
new java.io.FileInputStream(tempFile.getFile()));
|
||||||
|
ZipInputStream zis = new ZipInputStream(bis)) {
|
||||||
|
if (zis.getNextEntry() == null) {
|
||||||
|
throw new IllegalArgumentException("Archive is empty or invalid ZIP");
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IllegalArgumentException("Invalid CBZ/ZIP archive", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
try (PDDocument document = pdfDocumentFactory.createNewDocument();
|
||||||
|
ZipFile zipFile = new ZipFile(tempFile.getFile())) {
|
||||||
|
Enumeration<? extends ZipEntry> entries = zipFile.entries();
|
||||||
|
List<ImageEntryData> imageEntries = new ArrayList<>();
|
||||||
|
while (entries.hasMoreElements()) {
|
||||||
|
ZipEntry entry = entries.nextElement();
|
||||||
|
if (!entry.isDirectory() && isImageFile(entry.getName())) {
|
||||||
|
try (InputStream is = zipFile.getInputStream(entry)) {
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
is.transferTo(baos);
|
||||||
|
imageEntries.add(
|
||||||
|
new ImageEntryData(entry.getName(), baos.toByteArray()));
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("Error reading image {}: {}", entry.getName(), e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
imageEntries.sort(
|
||||||
|
Comparator.comparing(ImageEntryData::name, new NaturalOrderComparator()));
|
||||||
|
|
||||||
|
if (imageEntries.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("No valid images found in the CBZ file");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (ImageEntryData imageEntry : imageEntries) {
|
||||||
|
try {
|
||||||
|
PDImageXObject pdImage =
|
||||||
|
PDImageXObject.createFromByteArray(
|
||||||
|
document, imageEntry.data(), imageEntry.name());
|
||||||
|
PDPage page =
|
||||||
|
new PDPage(
|
||||||
|
new PDRectangle(pdImage.getWidth(), pdImage.getHeight()));
|
||||||
|
document.addPage(page);
|
||||||
|
try (PDPageContentStream contentStream =
|
||||||
|
new PDPageContentStream(document, page)) {
|
||||||
|
contentStream.drawImage(pdImage, 0, 0);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn(
|
||||||
|
"Error processing image {}: {}", imageEntry.name(), e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.getNumberOfPages() == 0) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"No images could be processed from the CBZ file");
|
||||||
|
}
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
document.save(baos);
|
||||||
|
byte[] pdfBytes = baos.toByteArray();
|
||||||
|
|
||||||
|
// Apply Ghostscript optimization if requested
|
||||||
|
if (optimizeForEbook) {
|
||||||
|
try {
|
||||||
|
return GeneralUtils.optimizePdfWithGhostscript(pdfBytes);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("Ghostscript optimization failed, returning unoptimized PDF", e);
|
||||||
|
return pdfBytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pdfBytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateCbzFile(MultipartFile file) {
|
||||||
|
if (file == null || file.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("File cannot be null or empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
String filename = file.getOriginalFilename();
|
||||||
|
if (filename == null) {
|
||||||
|
throw new IllegalArgumentException("File must have a name");
|
||||||
|
}
|
||||||
|
|
||||||
|
String extension = FilenameUtils.getExtension(filename).toLowerCase();
|
||||||
|
if (!"cbz".equals(extension) && !"zip".equals(extension)) {
|
||||||
|
throw new IllegalArgumentException("File must be a CBZ or ZIP archive");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isCbzFile(MultipartFile file) {
|
||||||
|
String filename = file.getOriginalFilename();
|
||||||
|
if (filename == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String extension = FilenameUtils.getExtension(filename).toLowerCase();
|
||||||
|
return "cbz".equals(extension) || "zip".equals(extension);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isComicBookFile(MultipartFile file) {
|
||||||
|
String filename = file.getOriginalFilename();
|
||||||
|
if (filename == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String extension = FilenameUtils.getExtension(filename).toLowerCase();
|
||||||
|
return "cbz".equals(extension)
|
||||||
|
|| "zip".equals(extension)
|
||||||
|
|| "cbr".equals(extension)
|
||||||
|
|| "rar".equals(extension);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isImageFile(String filename) {
|
||||||
|
return RegexPatternUtils.getInstance().getImageFilePattern().matcher(filename).matches();
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ImageEntryData(String name, byte[] data) {}
|
||||||
|
|
||||||
|
private class NaturalOrderComparator implements Comparator<String> {
|
||||||
|
@Override
|
||||||
|
public int compare(String s1, String s2) {
|
||||||
|
int len1 = s1.length();
|
||||||
|
int len2 = s2.length();
|
||||||
|
int marker1 = 0, marker2 = 0;
|
||||||
|
|
||||||
|
while (marker1 < len1 && marker2 < len2) {
|
||||||
|
String chunk1 = getChunk(s1, len1, marker1);
|
||||||
|
marker1 += chunk1.length();
|
||||||
|
|
||||||
|
String chunk2 = getChunk(s2, len2, marker2);
|
||||||
|
marker2 += chunk2.length();
|
||||||
|
|
||||||
|
int result;
|
||||||
|
if (isDigit(chunk1.charAt(0)) && isDigit(chunk2.charAt(0))) {
|
||||||
|
int thisNumericValue = Integer.parseInt(chunk1);
|
||||||
|
int thatNumericValue = Integer.parseInt(chunk2);
|
||||||
|
result = Integer.compare(thisNumericValue, thatNumericValue);
|
||||||
|
} else {
|
||||||
|
result = chunk1.compareTo(chunk2);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result != 0) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Integer.compare(len1, len2);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getChunk(String s, int length, int marker) {
|
||||||
|
StringBuilder chunk = new StringBuilder();
|
||||||
|
char c = s.charAt(marker);
|
||||||
|
chunk.append(c);
|
||||||
|
marker++;
|
||||||
|
|
||||||
|
if (isDigit(c)) {
|
||||||
|
while (marker < length && isDigit(s.charAt(marker))) {
|
||||||
|
chunk.append(s.charAt(marker));
|
||||||
|
marker++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
while (marker < length && !isDigit(s.charAt(marker))) {
|
||||||
|
chunk.append(s.charAt(marker));
|
||||||
|
marker++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return chunk.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isDigit(char ch) {
|
||||||
|
return ch >= '0' && ch <= '9';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,301 @@
|
|||||||
|
package stirling.software.common.util;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.zip.Adler32;
|
||||||
|
import java.util.zip.CRC32;
|
||||||
|
import java.util.zip.Checksum;
|
||||||
|
|
||||||
|
import lombok.experimental.UtilityClass;
|
||||||
|
|
||||||
|
@UtilityClass
|
||||||
|
public class ChecksumUtils {
|
||||||
|
|
||||||
|
/** Shared buffer size for streaming I/O. */
|
||||||
|
private static final int BUFFER_SIZE = 8192;
|
||||||
|
|
||||||
|
/** Mask to extract the lower 32 bits of a long value (unsigned int). */
|
||||||
|
private static final long UNSIGNED_32_BIT_MASK = 0xFFFFFFFFL;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes a checksum for the given file using the chosen algorithm and returns a lowercase hex
|
||||||
|
* string.
|
||||||
|
*
|
||||||
|
* <p>For digest algorithms (e.g., SHA-256, SHA-1, MD5), this returns the digest as hex. For
|
||||||
|
* 32-bit {@link Checksum} algorithms ("CRC32", "ADLER32"), this returns an 8-character
|
||||||
|
* lowercase hex string of the unsigned 32-bit value.
|
||||||
|
*
|
||||||
|
* @param path file to read
|
||||||
|
* @param algorithm algorithm name (case-insensitive). Special values: "CRC32", "ADLER32".
|
||||||
|
* @return hex string of the checksum
|
||||||
|
* @throws IOException if the file cannot be read
|
||||||
|
*/
|
||||||
|
public static String checksum(Path path, String algorithm) throws IOException {
|
||||||
|
try (InputStream is = Files.newInputStream(path)) {
|
||||||
|
return checksum(is, algorithm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes a checksum for the given stream using the chosen algorithm and returns a lowercase
|
||||||
|
* hex string.
|
||||||
|
*
|
||||||
|
* <p><strong>Note:</strong> This method does <em>not</em> close the provided stream.
|
||||||
|
*
|
||||||
|
* @param is input stream (not closed by this method)
|
||||||
|
* @param algorithm algorithm name (case-insensitive). Special values: "CRC32", "ADLER32".
|
||||||
|
* @return hex string of the checksum
|
||||||
|
* @throws IOException if reading from the stream fails
|
||||||
|
*/
|
||||||
|
public static String checksum(InputStream is, String algorithm) throws IOException {
|
||||||
|
switch (algorithm.toUpperCase(Locale.ROOT)) {
|
||||||
|
case "CRC32":
|
||||||
|
return checksumChecksum(is, new CRC32());
|
||||||
|
case "ADLER32":
|
||||||
|
return checksumChecksum(is, new Adler32());
|
||||||
|
default:
|
||||||
|
return toHex(checksumBytes(is, algorithm));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes a checksum for the given file using the chosen algorithm and returns a Base64
|
||||||
|
* encoded string.
|
||||||
|
*
|
||||||
|
* <p>For digest algorithms this is the Base64 of the raw digest bytes. For 32-bit checksum
|
||||||
|
* algorithms ("CRC32", "ADLER32"), this is the Base64 of the 4-byte big-endian unsigned value.
|
||||||
|
*
|
||||||
|
* @param path file to read
|
||||||
|
* @param algorithm algorithm name (case-insensitive). Special values: "CRC32", "ADLER32".
|
||||||
|
* @return Base64-encoded checksum bytes
|
||||||
|
* @throws IOException if the file cannot be read
|
||||||
|
*/
|
||||||
|
public static String checksumBase64(Path path, String algorithm) throws IOException {
|
||||||
|
try (InputStream is = Files.newInputStream(path)) {
|
||||||
|
return checksumBase64(is, algorithm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes a checksum for the given stream using the chosen algorithm and returns a Base64
|
||||||
|
* encoded string.
|
||||||
|
*
|
||||||
|
* <p><strong>Note:</strong> This method does <em>not</em> close the provided stream.
|
||||||
|
*
|
||||||
|
* @param is input stream (not closed by this method)
|
||||||
|
* @param algorithm algorithm name (case-insensitive). Special values: "CRC32", "ADLER32".
|
||||||
|
* @return Base64-encoded checksum bytes
|
||||||
|
* @throws IOException if reading from the stream fails
|
||||||
|
*/
|
||||||
|
public static String checksumBase64(InputStream is, String algorithm) throws IOException {
|
||||||
|
switch (algorithm.toUpperCase(Locale.ROOT)) {
|
||||||
|
case "CRC32":
|
||||||
|
return Base64.getEncoder().encodeToString(checksumChecksumBytes(is, new CRC32()));
|
||||||
|
case "ADLER32":
|
||||||
|
return Base64.getEncoder().encodeToString(checksumChecksumBytes(is, new Adler32()));
|
||||||
|
default:
|
||||||
|
return Base64.getEncoder().encodeToString(checksumBytes(is, algorithm));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes multiple checksums for the given file in a single pass over the data.
|
||||||
|
*
|
||||||
|
* <p>Returns a map from algorithm name to lowercase hex string. Order of results follows the
|
||||||
|
* order of the provided {@code algorithms}.
|
||||||
|
*
|
||||||
|
* @param path file to read
|
||||||
|
* @param algorithms algorithm names (case-insensitive). Special: "CRC32", "ADLER32".
|
||||||
|
* @return map of algorithm → hex string
|
||||||
|
* @throws IOException if the file cannot be read
|
||||||
|
*/
|
||||||
|
public static Map<String, String> checksums(Path path, String... algorithms)
|
||||||
|
throws IOException {
|
||||||
|
try (InputStream is = Files.newInputStream(path)) {
|
||||||
|
return checksums(is, algorithms);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes multiple checksums for the given stream in a single pass over the data.
|
||||||
|
*
|
||||||
|
* <p><strong>Note:</strong> This method does <em>not</em> close the provided stream.
|
||||||
|
*
|
||||||
|
* @param is input stream (not closed by this method)
|
||||||
|
* @param algorithms algorithm names (case-insensitive). Special: "CRC32", "ADLER32".
|
||||||
|
* @return map of algorithm → hex string
|
||||||
|
* @throws IOException if reading from the stream fails
|
||||||
|
*/
|
||||||
|
public static Map<String, String> checksums(InputStream is, String... algorithms)
|
||||||
|
throws IOException {
|
||||||
|
// Use LinkedHashMap to preserve the order of requested algorithms in the result.
|
||||||
|
Map<String, MessageDigest> digests = new LinkedHashMap<>();
|
||||||
|
Map<String, Checksum> checksums = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
for (String algorithm : algorithms) {
|
||||||
|
String key = algorithm; // keep original key for output
|
||||||
|
switch (algorithm.toUpperCase(Locale.ROOT)) {
|
||||||
|
case "CRC32":
|
||||||
|
checksums.put(key, new CRC32());
|
||||||
|
break;
|
||||||
|
case "ADLER32":
|
||||||
|
checksums.put(key, new Adler32());
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
try {
|
||||||
|
// For MessageDigest, pass the original name (case-insensitive per JCA)
|
||||||
|
digests.put(key, MessageDigest.getInstance(algorithm));
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new IllegalStateException("Unsupported algorithm: " + algorithm, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] buffer = new byte[BUFFER_SIZE];
|
||||||
|
int read;
|
||||||
|
while ((read = is.read(buffer)) != -1) {
|
||||||
|
for (MessageDigest digest : digests.values()) {
|
||||||
|
digest.update(buffer, 0, read);
|
||||||
|
}
|
||||||
|
for (Checksum cs : checksums.values()) {
|
||||||
|
cs.update(buffer, 0, read);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, String> results = new LinkedHashMap<>();
|
||||||
|
for (Map.Entry<String, MessageDigest> entry : digests.entrySet()) {
|
||||||
|
results.put(entry.getKey(), toHex(entry.getValue().digest()));
|
||||||
|
}
|
||||||
|
for (Map.Entry<String, Checksum> entry : checksums.entrySet()) {
|
||||||
|
// Keep value as long and mask to ensure unsigned hex formatting.
|
||||||
|
long unsigned32 = entry.getValue().getValue() & UNSIGNED_32_BIT_MASK;
|
||||||
|
results.put(entry.getKey(), String.format("%08x", unsigned32));
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares the checksum of a file with an expected hex string (case-insensitive).
|
||||||
|
*
|
||||||
|
* @param path file to read
|
||||||
|
* @param algorithm algorithm name (case-insensitive). Special: "CRC32", "ADLER32".
|
||||||
|
* @param expected expected hex string (case-insensitive)
|
||||||
|
* @return {@code true} if they match, otherwise {@code false}
|
||||||
|
* @throws IOException if the file cannot be read
|
||||||
|
*/
|
||||||
|
public static boolean matches(Path path, String algorithm, String expected) throws IOException {
|
||||||
|
try (InputStream is = Files.newInputStream(path)) {
|
||||||
|
return matches(is, algorithm, expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares the checksum of a stream with an expected hex string (case-insensitive).
|
||||||
|
*
|
||||||
|
* <p><strong>Note:</strong> This method does <em>not</em> close the provided stream.
|
||||||
|
*
|
||||||
|
* @param is input stream (not closed by this method)
|
||||||
|
* @param algorithm algorithm name (case-insensitive). Special: "CRC32", "ADLER32".
|
||||||
|
* @param expected expected hex string (case-insensitive)
|
||||||
|
* @return {@code true} if they match, otherwise {@code false}
|
||||||
|
* @throws IOException if reading from the stream fails
|
||||||
|
*/
|
||||||
|
public static boolean matches(InputStream is, String algorithm, String expected)
|
||||||
|
throws IOException {
|
||||||
|
return checksum(is, algorithm).equalsIgnoreCase(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Internal helpers ----------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes a MessageDigest over a stream and returns the raw digest bytes.
|
||||||
|
*
|
||||||
|
* @param is input stream (not closed)
|
||||||
|
* @param algorithm JCA MessageDigest algorithm (e.g., "SHA-256")
|
||||||
|
* @return raw digest bytes
|
||||||
|
* @throws IOException if reading fails
|
||||||
|
* @throws IllegalStateException if the algorithm is unsupported
|
||||||
|
*/
|
||||||
|
private static byte[] checksumBytes(InputStream is, String algorithm) throws IOException {
|
||||||
|
try {
|
||||||
|
MessageDigest digest = MessageDigest.getInstance(algorithm);
|
||||||
|
byte[] buffer = new byte[BUFFER_SIZE];
|
||||||
|
int read;
|
||||||
|
while ((read = is.read(buffer)) != -1) {
|
||||||
|
digest.update(buffer, 0, read);
|
||||||
|
}
|
||||||
|
return digest.digest();
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
// Keep the message explicit to aid debugging
|
||||||
|
throw new IllegalStateException("Unsupported algorithm: " + algorithm, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes a 32-bit {@link Checksum} over a stream and returns the lowercase 8-char hex of the
|
||||||
|
* unsigned 32-bit value.
|
||||||
|
*
|
||||||
|
* @param is input stream (not closed)
|
||||||
|
* @param checksum checksum implementation (CRC32, Adler32, etc.)
|
||||||
|
* @return 8-character lowercase hex (big-endian representation)
|
||||||
|
* @throws IOException if reading fails
|
||||||
|
*/
|
||||||
|
private static String checksumChecksum(InputStream is, Checksum checksum) throws IOException {
|
||||||
|
byte[] buffer = new byte[BUFFER_SIZE];
|
||||||
|
int read;
|
||||||
|
while ((read = is.read(buffer)) != -1) {
|
||||||
|
checksum.update(buffer, 0, read);
|
||||||
|
}
|
||||||
|
// Keep as long and mask to ensure correct unsigned representation.
|
||||||
|
long unsigned32 = checksum.getValue() & UNSIGNED_32_BIT_MASK;
|
||||||
|
return String.format("%08x", unsigned32);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes a 32-bit {@link Checksum} over a stream and returns the raw 4-byte big-endian
|
||||||
|
* representation of the unsigned 32-bit value.
|
||||||
|
*
|
||||||
|
* <p>Cast to int already truncates to the lower 32 bits; the sign is irrelevant because we
|
||||||
|
* serialize the bit pattern directly into 4 bytes.
|
||||||
|
*
|
||||||
|
* @param is input stream (not closed)
|
||||||
|
* @param checksum checksum implementation (CRC32, Adler32, etc.)
|
||||||
|
* @return 4 bytes (big-endian)
|
||||||
|
* @throws IOException if reading fails
|
||||||
|
*/
|
||||||
|
private static byte[] checksumChecksumBytes(InputStream is, Checksum checksum)
|
||||||
|
throws IOException {
|
||||||
|
byte[] buffer = new byte[BUFFER_SIZE];
|
||||||
|
int read;
|
||||||
|
while ((read = is.read(buffer)) != -1) {
|
||||||
|
checksum.update(buffer, 0, read);
|
||||||
|
}
|
||||||
|
// Cast keeps only the lower 32 bits; mask is unnecessary here.
|
||||||
|
int v = (int) checksum.getValue();
|
||||||
|
return ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(v).array();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts bytes to a lowercase hex string.
|
||||||
|
*
|
||||||
|
* @param hash the byte array to convert
|
||||||
|
* @return the lowercase hex string
|
||||||
|
*/
|
||||||
|
private static String toHex(byte[] hash) {
|
||||||
|
StringBuilder sb = new StringBuilder(hash.length * 2);
|
||||||
|
for (byte b : hash) {
|
||||||
|
sb.append(String.format("%02x", b));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,6 @@ import org.owasp.html.AttributePolicy;
|
|||||||
import org.owasp.html.HtmlPolicyBuilder;
|
import org.owasp.html.HtmlPolicyBuilder;
|
||||||
import org.owasp.html.PolicyFactory;
|
import org.owasp.html.PolicyFactory;
|
||||||
import org.owasp.html.Sanitizers;
|
import org.owasp.html.Sanitizers;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import stirling.software.common.model.ApplicationProperties;
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
@@ -16,7 +15,6 @@ public class CustomHtmlSanitizer {
|
|||||||
private final SsrfProtectionService ssrfProtectionService;
|
private final SsrfProtectionService ssrfProtectionService;
|
||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
@Autowired
|
|
||||||
public CustomHtmlSanitizer(
|
public CustomHtmlSanitizer(
|
||||||
SsrfProtectionService ssrfProtectionService,
|
SsrfProtectionService ssrfProtectionService,
|
||||||
ApplicationProperties applicationProperties) {
|
ApplicationProperties applicationProperties) {
|
||||||
@@ -28,7 +26,7 @@ public class CustomHtmlSanitizer {
|
|||||||
new AttributePolicy() {
|
new AttributePolicy() {
|
||||||
@Override
|
@Override
|
||||||
public String apply(String elementName, String attributeName, String value) {
|
public String apply(String elementName, String attributeName, String value) {
|
||||||
if (value == null || value.trim().isEmpty()) {
|
if (value.trim().isEmpty()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,16 @@ import java.io.InputStream;
|
|||||||
import java.lang.reflect.Constructor;
|
import java.lang.reflect.Constructor;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.experimental.UtilityClass;
|
import lombok.experimental.UtilityClass;
|
||||||
|
|
||||||
@@ -25,11 +29,11 @@ public class EmlParser {
|
|||||||
private static volatile boolean mimeUtilityChecked = false;
|
private static volatile boolean mimeUtilityChecked = false;
|
||||||
|
|
||||||
private static final Pattern MIME_ENCODED_PATTERN =
|
private static final Pattern MIME_ENCODED_PATTERN =
|
||||||
Pattern.compile("=\\?([^?]+)\\?([BbQq])\\?([^?]*)\\?=");
|
RegexPatternUtils.getInstance().getMimeEncodedWordPattern();
|
||||||
|
|
||||||
private static final String DISPOSITION_ATTACHMENT = "attachment";
|
private static final String DISPOSITION_ATTACHMENT = "attachment";
|
||||||
private static final String TEXT_PLAIN = "text/plain";
|
private static final String TEXT_PLAIN = MediaType.TEXT_PLAIN_VALUE;
|
||||||
private static final String TEXT_HTML = "text/html";
|
private static final String TEXT_HTML = MediaType.TEXT_HTML_VALUE;
|
||||||
private static final String MULTIPART_PREFIX = "multipart/";
|
private static final String MULTIPART_PREFIX = "multipart/";
|
||||||
|
|
||||||
private static final String HEADER_CONTENT_TYPE = "content-type:";
|
private static final String HEADER_CONTENT_TYPE = "content-type:";
|
||||||
@@ -69,12 +73,12 @@ public class EmlParser {
|
|||||||
if (isJakartaMailAvailable()) {
|
if (isJakartaMailAvailable()) {
|
||||||
return extractEmailContentAdvanced(emlBytes, request, customHtmlSanitizer);
|
return extractEmailContentAdvanced(emlBytes, request, customHtmlSanitizer);
|
||||||
} else {
|
} else {
|
||||||
return extractEmailContentBasic(emlBytes, request, customHtmlSanitizer);
|
return extractEmailContentBasic(emlBytes, customHtmlSanitizer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static EmailContent extractEmailContentBasic(
|
private static EmailContent extractEmailContentBasic(
|
||||||
byte[] emlBytes, EmlToPdfRequest request, CustomHtmlSanitizer customHtmlSanitizer) {
|
byte[] emlBytes, CustomHtmlSanitizer customHtmlSanitizer) {
|
||||||
String emlContent = new String(emlBytes, StandardCharsets.UTF_8);
|
String emlContent = new String(emlBytes, StandardCharsets.UTF_8);
|
||||||
EmailContent content = new EmailContent();
|
EmailContent content = new EmailContent();
|
||||||
|
|
||||||
@@ -121,7 +125,7 @@ public class EmlParser {
|
|||||||
return extractFromMimeMessage(message, request, customHtmlSanitizer);
|
return extractFromMimeMessage(message, request, customHtmlSanitizer);
|
||||||
|
|
||||||
} catch (ReflectiveOperationException e) {
|
} catch (ReflectiveOperationException e) {
|
||||||
return extractEmailContentBasic(emlBytes, request, customHtmlSanitizer);
|
return extractEmailContentBasic(emlBytes, customHtmlSanitizer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,7 +147,11 @@ public class EmlParser {
|
|||||||
extractRecipients(message, messageClass, content);
|
extractRecipients(message, messageClass, content);
|
||||||
|
|
||||||
Method getSentDate = messageClass.getMethod("getSentDate");
|
Method getSentDate = messageClass.getMethod("getSentDate");
|
||||||
content.setDate((Date) getSentDate.invoke(message));
|
Date legacyDate = (Date) getSentDate.invoke(message);
|
||||||
|
if (legacyDate != null) {
|
||||||
|
content.setDate(
|
||||||
|
ZonedDateTime.ofInstant(legacyDate.toInstant(), ZoneId.systemDefault()));
|
||||||
|
}
|
||||||
|
|
||||||
Method getContent = messageClass.getMethod("getContent");
|
Method getContent = messageClass.getMethod("getContent");
|
||||||
Object messageContent = getContent.invoke(message);
|
Object messageContent = getContent.invoke(message);
|
||||||
@@ -349,7 +357,11 @@ public class EmlParser {
|
|||||||
for (String contentIdHeader : contentIdHeaders) {
|
for (String contentIdHeader : contentIdHeaders) {
|
||||||
if (contentIdHeader != null && !contentIdHeader.trim().isEmpty()) {
|
if (contentIdHeader != null && !contentIdHeader.trim().isEmpty()) {
|
||||||
attachment.setEmbedded(true);
|
attachment.setEmbedded(true);
|
||||||
String contentId = contentIdHeader.trim().replaceAll("[<>]", "");
|
String contentId =
|
||||||
|
RegexPatternUtils.getInstance()
|
||||||
|
.getAngleBracketsPattern()
|
||||||
|
.matcher(contentIdHeader.trim())
|
||||||
|
.replaceAll("");
|
||||||
attachment.setContentId(contentId);
|
attachment.setContentId(contentId);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -406,7 +418,8 @@ public class EmlParser {
|
|||||||
|
|
||||||
private static String extractBasicHeader(String emlContent, String headerName) {
|
private static String extractBasicHeader(String emlContent, String headerName) {
|
||||||
try {
|
try {
|
||||||
String[] lines = emlContent.split("\r?\n");
|
String[] lines =
|
||||||
|
RegexPatternUtils.getInstance().getNewlineSplitPattern().split(emlContent);
|
||||||
for (int i = 0; i < lines.length; i++) {
|
for (int i = 0; i < lines.length; i++) {
|
||||||
String line = lines[i];
|
String line = lines[i];
|
||||||
if (line.toLowerCase().startsWith(headerName.toLowerCase())) {
|
if (line.toLowerCase().startsWith(headerName.toLowerCase())) {
|
||||||
@@ -477,7 +490,10 @@ public class EmlParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static int findPartEnd(String content, int start) {
|
private static int findPartEnd(String content, int start) {
|
||||||
String[] lines = content.substring(start).split("\r?\n");
|
String[] lines =
|
||||||
|
RegexPatternUtils.getInstance()
|
||||||
|
.getNewlineSplitPattern()
|
||||||
|
.split(content.substring(start));
|
||||||
StringBuilder result = new StringBuilder();
|
StringBuilder result = new StringBuilder();
|
||||||
|
|
||||||
for (String line : lines) {
|
for (String line : lines) {
|
||||||
@@ -491,7 +507,8 @@ public class EmlParser {
|
|||||||
private static List<EmailAttachment> extractAttachmentsBasic(String emlContent) {
|
private static List<EmailAttachment> extractAttachmentsBasic(String emlContent) {
|
||||||
List<EmailAttachment> attachments = new ArrayList<>();
|
List<EmailAttachment> attachments = new ArrayList<>();
|
||||||
try {
|
try {
|
||||||
String[] lines = emlContent.split("\r?\n");
|
String[] lines =
|
||||||
|
RegexPatternUtils.getInstance().getNewlineSplitPattern().split(emlContent);
|
||||||
boolean inHeaders = true;
|
boolean inHeaders = true;
|
||||||
String currentContentType = "";
|
String currentContentType = "";
|
||||||
String currentDisposition = "";
|
String currentDisposition = "";
|
||||||
@@ -554,7 +571,11 @@ public class EmlParser {
|
|||||||
if (filenameStarEnd == -1) filenameStarEnd = disposition.length();
|
if (filenameStarEnd == -1) filenameStarEnd = disposition.length();
|
||||||
String extendedFilename =
|
String extendedFilename =
|
||||||
disposition.substring(filenameStarStart, filenameStarEnd).trim();
|
disposition.substring(filenameStarStart, filenameStarEnd).trim();
|
||||||
extendedFilename = extendedFilename.replaceAll("^\"|\"$", "");
|
extendedFilename =
|
||||||
|
RegexPatternUtils.getInstance()
|
||||||
|
.getQuotesRemovalPattern()
|
||||||
|
.matcher(extendedFilename)
|
||||||
|
.replaceAll("");
|
||||||
|
|
||||||
if (extendedFilename.contains("'")) {
|
if (extendedFilename.contains("'")) {
|
||||||
String[] parts = extendedFilename.split("'", 3);
|
String[] parts = extendedFilename.split("'", 3);
|
||||||
@@ -569,7 +590,11 @@ public class EmlParser {
|
|||||||
int filenameEnd = disposition.indexOf(";", filenameStart);
|
int filenameEnd = disposition.indexOf(";", filenameStart);
|
||||||
if (filenameEnd == -1) filenameEnd = disposition.length();
|
if (filenameEnd == -1) filenameEnd = disposition.length();
|
||||||
String filename = disposition.substring(filenameStart, filenameEnd).trim();
|
String filename = disposition.substring(filenameStart, filenameEnd).trim();
|
||||||
filename = filename.replaceAll("^\"|\"$", "");
|
filename =
|
||||||
|
RegexPatternUtils.getInstance()
|
||||||
|
.getQuotesRemovalPattern()
|
||||||
|
.matcher(filename)
|
||||||
|
.replaceAll("");
|
||||||
return safeMimeDecode(filename);
|
return safeMimeDecode(filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -614,7 +639,7 @@ public class EmlParser {
|
|||||||
private String to;
|
private String to;
|
||||||
private String cc;
|
private String cc;
|
||||||
private String bcc;
|
private String bcc;
|
||||||
private Date date;
|
private ZonedDateTime date;
|
||||||
private String dateString; // For basic parsing fallback
|
private String dateString; // For basic parsing fallback
|
||||||
private String htmlBody;
|
private String htmlBody;
|
||||||
private String textBody;
|
private String textBody;
|
||||||
@@ -622,11 +647,23 @@ public class EmlParser {
|
|||||||
private List<EmailAttachment> attachments = new ArrayList<>();
|
private List<EmailAttachment> attachments = new ArrayList<>();
|
||||||
|
|
||||||
public void setHtmlBody(String htmlBody) {
|
public void setHtmlBody(String htmlBody) {
|
||||||
this.htmlBody = htmlBody != null ? htmlBody.replaceAll("\r", "") : null;
|
this.htmlBody =
|
||||||
|
htmlBody != null
|
||||||
|
? RegexPatternUtils.getInstance()
|
||||||
|
.getCarriageReturnPattern()
|
||||||
|
.matcher(htmlBody)
|
||||||
|
.replaceAll("")
|
||||||
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setTextBody(String textBody) {
|
public void setTextBody(String textBody) {
|
||||||
this.textBody = textBody != null ? textBody.replaceAll("\r", "") : null;
|
this.textBody =
|
||||||
|
textBody != null
|
||||||
|
? RegexPatternUtils.getInstance()
|
||||||
|
.getCarriageReturnPattern()
|
||||||
|
.matcher(textBody)
|
||||||
|
.replaceAll("")
|
||||||
|
: null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import java.util.Map;
|
|||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
|
||||||
import lombok.experimental.UtilityClass;
|
import lombok.experimental.UtilityClass;
|
||||||
|
|
||||||
import stirling.software.common.model.api.converters.EmlToPdfRequest;
|
import stirling.software.common.model.api.converters.EmlToPdfRequest;
|
||||||
@@ -33,10 +35,10 @@ public class EmlProcessingUtils {
|
|||||||
// MIME type detection
|
// MIME type detection
|
||||||
private static final Map<String, String> EXTENSION_TO_MIME_TYPE =
|
private static final Map<String, String> EXTENSION_TO_MIME_TYPE =
|
||||||
Map.of(
|
Map.of(
|
||||||
".png", "image/png",
|
".png", MediaType.IMAGE_PNG_VALUE,
|
||||||
".jpg", "image/jpeg",
|
".jpg", MediaType.IMAGE_JPEG_VALUE,
|
||||||
".jpeg", "image/jpeg",
|
".jpeg", MediaType.IMAGE_JPEG_VALUE,
|
||||||
".gif", "image/gif",
|
".gif", MediaType.IMAGE_GIF_VALUE,
|
||||||
".bmp", "image/bmp",
|
".bmp", "image/bmp",
|
||||||
".webp", "image/webp",
|
".webp", "image/webp",
|
||||||
".svg", "image/svg+xml",
|
".svg", "image/svg+xml",
|
||||||
@@ -81,8 +83,8 @@ public class EmlProcessingUtils {
|
|||||||
|| lowerContent.contains("bcc:");
|
|| lowerContent.contains("bcc:");
|
||||||
boolean hasMimeStructure =
|
boolean hasMimeStructure =
|
||||||
lowerContent.contains("multipart/")
|
lowerContent.contains("multipart/")
|
||||||
|| lowerContent.contains("text/plain")
|
|| lowerContent.contains(MediaType.TEXT_PLAIN_VALUE)
|
||||||
|| lowerContent.contains("text/html")
|
|| lowerContent.contains(MediaType.TEXT_HTML_VALUE)
|
||||||
|| lowerContent.contains("boundary=");
|
|| lowerContent.contains("boundary=");
|
||||||
|
|
||||||
int headerCount = 0;
|
int headerCount = 0;
|
||||||
@@ -197,8 +199,16 @@ public class EmlProcessingUtils {
|
|||||||
String processed =
|
String processed =
|
||||||
customHtmlSanitizer != null ? customHtmlSanitizer.sanitize(htmlBody) : htmlBody;
|
customHtmlSanitizer != null ? customHtmlSanitizer.sanitize(htmlBody) : htmlBody;
|
||||||
|
|
||||||
processed = processed.replaceAll("(?i)\\s*position\\s*:\\s*fixed[^;]*;?", "");
|
processed =
|
||||||
processed = processed.replaceAll("(?i)\\s*position\\s*:\\s*absolute[^;]*;?", "");
|
RegexPatternUtils.getInstance()
|
||||||
|
.getFixedPositionCssPattern()
|
||||||
|
.matcher(processed)
|
||||||
|
.replaceAll("");
|
||||||
|
processed =
|
||||||
|
RegexPatternUtils.getInstance()
|
||||||
|
.getAbsolutePositionCssPattern()
|
||||||
|
.matcher(processed)
|
||||||
|
.replaceAll("");
|
||||||
|
|
||||||
if (emailContent != null && !emailContent.getAttachments().isEmpty()) {
|
if (emailContent != null && !emailContent.getAttachments().isEmpty()) {
|
||||||
processed = PdfAttachmentHandler.processInlineImages(processed, emailContent);
|
processed = PdfAttachmentHandler.processInlineImages(processed, emailContent);
|
||||||
@@ -220,13 +230,17 @@ public class EmlProcessingUtils {
|
|||||||
html = html.replace("\n", "<br>\n");
|
html = html.replace("\n", "<br>\n");
|
||||||
|
|
||||||
html =
|
html =
|
||||||
html.replaceAll(
|
RegexPatternUtils.getInstance()
|
||||||
"(https?://[\\w\\-._~:/?#\\[\\]@!$&'()*+,;=%]+)",
|
.getUrlLinkPattern()
|
||||||
|
.matcher(html)
|
||||||
|
.replaceAll(
|
||||||
"<a href=\"$1\" style=\"color: #1a73e8; text-decoration: underline;\">$1</a>");
|
"<a href=\"$1\" style=\"color: #1a73e8; text-decoration: underline;\">$1</a>");
|
||||||
|
|
||||||
html =
|
html =
|
||||||
html.replaceAll(
|
RegexPatternUtils.getInstance()
|
||||||
"([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,63})",
|
.getEmailLinkPattern()
|
||||||
|
.matcher(html)
|
||||||
|
.replaceAll(
|
||||||
"<a href=\"mailto:$1\" style=\"color: #1a73e8; text-decoration: underline;\">$1</a>");
|
"<a href=\"mailto:$1\" style=\"color: #1a73e8; text-decoration: underline;\">$1</a>");
|
||||||
|
|
||||||
return html;
|
return html;
|
||||||
@@ -464,7 +478,7 @@ public class EmlProcessingUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return "image/png";
|
return MediaType.IMAGE_PNG_VALUE; // Default MIME type
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String decodeUrlEncoded(String encoded) {
|
public static String decodeUrlEncoded(String encoded) {
|
||||||
@@ -488,9 +502,13 @@ public class EmlProcessingUtils {
|
|||||||
Matcher concatenatedMatcher = concatenatedPattern.matcher(encodedText);
|
Matcher concatenatedMatcher = concatenatedPattern.matcher(encodedText);
|
||||||
String processedText =
|
String processedText =
|
||||||
concatenatedMatcher.replaceAll(
|
concatenatedMatcher.replaceAll(
|
||||||
match -> match.group().replaceAll("\\s+(?==\\?)", ""));
|
match ->
|
||||||
|
RegexPatternUtils.getInstance()
|
||||||
|
.getMimeHeaderWhitespacePattern()
|
||||||
|
.matcher(match.group())
|
||||||
|
.replaceAll(""));
|
||||||
|
|
||||||
Pattern mimePattern = Pattern.compile("=\\?([^?]+)\\?([BbQq])\\?([^?]*)\\?=");
|
Pattern mimePattern = RegexPatternUtils.getInstance().getMimeEncodedWordPattern();
|
||||||
Matcher matcher = mimePattern.matcher(processedText);
|
Matcher matcher = mimePattern.matcher(processedText);
|
||||||
int lastEnd = 0;
|
int lastEnd = 0;
|
||||||
|
|
||||||
@@ -505,7 +523,11 @@ public class EmlProcessingUtils {
|
|||||||
String decodedValue =
|
String decodedValue =
|
||||||
switch (encoding) {
|
switch (encoding) {
|
||||||
case "B" -> {
|
case "B" -> {
|
||||||
String cleanBase64 = encodedValue.replaceAll("\\s", "");
|
String cleanBase64 =
|
||||||
|
RegexPatternUtils.getInstance()
|
||||||
|
.getWhitespacePattern()
|
||||||
|
.matcher(encodedValue)
|
||||||
|
.replaceAll("");
|
||||||
byte[] decodedBytes = Base64.getDecoder().decode(cleanBase64);
|
byte[] decodedBytes = Base64.getDecoder().decode(cleanBase64);
|
||||||
Charset targetCharset;
|
Charset targetCharset;
|
||||||
try {
|
try {
|
||||||
@@ -594,8 +616,16 @@ public class EmlProcessingUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static String simplifyHtmlContent(String htmlContent) {
|
public static String simplifyHtmlContent(String htmlContent) {
|
||||||
String simplified = htmlContent.replaceAll("(?i)<script[^>]*>.*?</script>", "");
|
String simplified =
|
||||||
simplified = simplified.replaceAll("(?i)<style[^>]*>.*?</style>", "");
|
RegexPatternUtils.getInstance()
|
||||||
|
.getScriptTagPattern()
|
||||||
|
.matcher(htmlContent)
|
||||||
|
.replaceAll("");
|
||||||
|
simplified =
|
||||||
|
RegexPatternUtils.getInstance()
|
||||||
|
.getStyleTagPattern()
|
||||||
|
.matcher(simplified)
|
||||||
|
.replaceAll("");
|
||||||
return simplified;
|
return simplified;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -324,4 +324,63 @@ public class ExceptionUtils {
|
|||||||
return createIllegalArgumentException(
|
return createIllegalArgumentException(
|
||||||
"error.argumentRequired", "{0} must not be null", argumentName);
|
"error.argumentRequired", "{0} must not be null", argumentName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a RuntimeException for memory/image size errors when rendering PDF images with DPI.
|
||||||
|
* Handles OutOfMemoryError and related conditions (e.g., NegativeArraySizeException) that
|
||||||
|
* result from images exceeding Java's array/memory limits.
|
||||||
|
*
|
||||||
|
* @param pageNumber the page number that caused the error
|
||||||
|
* @param dpi the DPI value used
|
||||||
|
* @param cause the original error/exception (e.g., OutOfMemoryError,
|
||||||
|
* NegativeArraySizeException)
|
||||||
|
* @return RuntimeException with user-friendly message
|
||||||
|
*/
|
||||||
|
public static RuntimeException createOutOfMemoryDpiException(
|
||||||
|
int pageNumber, int dpi, Throwable cause) {
|
||||||
|
String message =
|
||||||
|
MessageFormat.format(
|
||||||
|
"Out of memory or image-too-large error while rendering PDF page {0} at {1} DPI. "
|
||||||
|
+ "This can occur when the resulting image exceeds Java's array/memory limits (e.g., NegativeArraySizeException). "
|
||||||
|
+ "Please use a lower DPI value (recommended: 150 or less) or process the document in smaller chunks.",
|
||||||
|
pageNumber, dpi);
|
||||||
|
return new RuntimeException(message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a RuntimeException for OutOfMemoryError when rendering PDF images with DPI.
|
||||||
|
*
|
||||||
|
* @param pageNumber the page number that caused the error
|
||||||
|
* @param dpi the DPI value used
|
||||||
|
* @param cause the original OutOfMemoryError
|
||||||
|
* @return RuntimeException with user-friendly message
|
||||||
|
*/
|
||||||
|
public static RuntimeException createOutOfMemoryDpiException(
|
||||||
|
int pageNumber, int dpi, OutOfMemoryError cause) {
|
||||||
|
return createOutOfMemoryDpiException(pageNumber, dpi, (Throwable) cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a RuntimeException for memory/image size errors when rendering PDF images with DPI.
|
||||||
|
* Handles OutOfMemoryError and related conditions (e.g., NegativeArraySizeException) that
|
||||||
|
* result from images exceeding Java's array/memory limits.
|
||||||
|
*
|
||||||
|
* @param dpi the DPI value used
|
||||||
|
* @param cause the original error/exception (e.g., OutOfMemoryError,
|
||||||
|
* NegativeArraySizeException)
|
||||||
|
* @return RuntimeException with user-friendly message
|
||||||
|
*/
|
||||||
|
public static RuntimeException createOutOfMemoryDpiException(int dpi, Throwable cause) {
|
||||||
|
String message =
|
||||||
|
MessageFormat.format(
|
||||||
|
"Out of memory or image-too-large error while rendering PDF at {0} DPI. "
|
||||||
|
+ "This can occur when the resulting image exceeds Java's array/memory limits (e.g., NegativeArraySizeException). "
|
||||||
|
+ "Please use a lower DPI value (recommended: 150 or less) or process the document in smaller chunks.",
|
||||||
|
dpi);
|
||||||
|
return new RuntimeException(message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RuntimeException createOutOfMemoryDpiException(int dpi, OutOfMemoryError cause) {
|
||||||
|
return createOutOfMemoryDpiException(dpi, (Throwable) cause);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package stirling.software.common.util;
|
package stirling.software.common.util;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.UncheckedIOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.FileVisitResult;
|
import java.nio.file.FileVisitResult;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
@@ -124,8 +127,8 @@ public class FileToPdf {
|
|||||||
private static void zipDirectory(Path sourceDir, Path zipFilePath) throws IOException {
|
private static void zipDirectory(Path sourceDir, Path zipFilePath) throws IOException {
|
||||||
try (ZipOutputStream zos =
|
try (ZipOutputStream zos =
|
||||||
new ZipOutputStream(new FileOutputStream(zipFilePath.toFile()))) {
|
new ZipOutputStream(new FileOutputStream(zipFilePath.toFile()))) {
|
||||||
Files.walk(sourceDir)
|
try (Stream<Path> walk = Files.walk(sourceDir)) {
|
||||||
.filter(path -> !Files.isDirectory(path))
|
walk.filter(path -> !Files.isDirectory(path))
|
||||||
.forEach(
|
.forEach(
|
||||||
path -> {
|
path -> {
|
||||||
ZipEntry zipEntry =
|
ZipEntry zipEntry =
|
||||||
@@ -140,6 +143,7 @@ public class FileToPdf {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void deleteDirectory(Path dir) throws IOException {
|
private static void deleteDirectory(Path dir) throws IOException {
|
||||||
Files.walkFileTree(
|
Files.walkFileTree(
|
||||||
@@ -204,15 +208,27 @@ public class FileToPdf {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
// Remove any drive letters (e.g., "C:\") and leading forward/backslashes
|
// Remove any drive letters (e.g., "C:\") and leading forward/backslashes
|
||||||
entryName = entryName.replaceAll("^[a-zA-Z]:[\\\\/]+", "");
|
entryName =
|
||||||
entryName = entryName.replaceAll("^[\\\\/]+", "");
|
RegexPatternUtils.getInstance()
|
||||||
|
.getDriveLetterPattern()
|
||||||
|
.matcher(entryName)
|
||||||
|
.replaceAll("");
|
||||||
|
entryName =
|
||||||
|
RegexPatternUtils.getInstance()
|
||||||
|
.getLeadingSlashesPattern()
|
||||||
|
.matcher(entryName)
|
||||||
|
.replaceAll("");
|
||||||
|
|
||||||
// Recursively remove path traversal sequences
|
// Recursively remove path traversal sequences
|
||||||
while (entryName.contains("../") || entryName.contains("..\\")) {
|
while (entryName.contains("../") || entryName.contains("..\\")) {
|
||||||
entryName = entryName.replace("../", "").replace("..\\", "");
|
entryName = entryName.replace("../", "").replace("..\\", "");
|
||||||
}
|
}
|
||||||
// Normalize all backslashes to forward slashes
|
// Normalize all backslashes to forward slashes
|
||||||
entryName = entryName.replaceAll("\\\\", "/");
|
entryName =
|
||||||
|
RegexPatternUtils.getInstance()
|
||||||
|
.getBackslashPattern()
|
||||||
|
.matcher(entryName)
|
||||||
|
.replaceAll("/");
|
||||||
return entryName;
|
return entryName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,658 @@
|
|||||||
|
package stirling.software.common.util;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.cos.COSName;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDResources;
|
||||||
|
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||||
|
import org.apache.pdfbox.pdmodel.font.PDType1Font;
|
||||||
|
import org.apache.pdfbox.pdmodel.font.Standard14Fonts;
|
||||||
|
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
|
||||||
|
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget;
|
||||||
|
import org.apache.pdfbox.pdmodel.interactive.form.*;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public final class FormUtils {
|
||||||
|
|
||||||
|
private FormUtils() {}
|
||||||
|
|
||||||
|
public static boolean hasAnyRotatedPage(PDDocument document) {
|
||||||
|
try {
|
||||||
|
for (PDPage page : document.getPages()) {
|
||||||
|
int rot = page.getRotation();
|
||||||
|
int norm = ((rot % 360) + 360) % 360;
|
||||||
|
if (norm != 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Failed to inspect page rotations: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void copyAndTransformFormFields(
|
||||||
|
PDDocument sourceDocument,
|
||||||
|
PDDocument newDocument,
|
||||||
|
int totalPages,
|
||||||
|
int pagesPerSheet,
|
||||||
|
int cols,
|
||||||
|
int rows,
|
||||||
|
float cellWidth,
|
||||||
|
float cellHeight)
|
||||||
|
throws IOException {
|
||||||
|
|
||||||
|
PDDocumentCatalog sourceCatalog = sourceDocument.getDocumentCatalog();
|
||||||
|
PDAcroForm sourceAcroForm = sourceCatalog.getAcroForm();
|
||||||
|
|
||||||
|
if (sourceAcroForm == null || sourceAcroForm.getFields().isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PDDocumentCatalog newCatalog = newDocument.getDocumentCatalog();
|
||||||
|
PDAcroForm newAcroForm = new PDAcroForm(newDocument);
|
||||||
|
newCatalog.setAcroForm(newAcroForm);
|
||||||
|
|
||||||
|
PDResources dr = new PDResources();
|
||||||
|
PDType1Font helvetica = new PDType1Font(Standard14Fonts.FontName.HELVETICA);
|
||||||
|
PDType1Font zapfDingbats = new PDType1Font(Standard14Fonts.FontName.ZAPF_DINGBATS);
|
||||||
|
dr.put(COSName.getPDFName("Helv"), helvetica);
|
||||||
|
dr.put(COSName.getPDFName("ZaDb"), zapfDingbats);
|
||||||
|
newAcroForm.setDefaultResources(dr);
|
||||||
|
newAcroForm.setDefaultAppearance("/Helv 12 Tf 0 g");
|
||||||
|
|
||||||
|
// Do not mutate the source AcroForm; skip bad widgets during copy
|
||||||
|
newAcroForm.setNeedAppearances(true);
|
||||||
|
|
||||||
|
Map<String, Integer> fieldNameCounters = new HashMap<>();
|
||||||
|
|
||||||
|
// Build widget -> field map once for efficient lookups
|
||||||
|
Map<PDAnnotationWidget, PDField> widgetFieldMap = buildWidgetFieldMap(sourceAcroForm);
|
||||||
|
|
||||||
|
for (int pageIndex = 0; pageIndex < totalPages; pageIndex++) {
|
||||||
|
PDPage sourcePage = sourceDocument.getPage(pageIndex);
|
||||||
|
List<PDAnnotation> annotations = sourcePage.getAnnotations();
|
||||||
|
|
||||||
|
if (annotations.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int destinationPageIndex = pageIndex / pagesPerSheet;
|
||||||
|
int adjustedPageIndex = pageIndex % pagesPerSheet;
|
||||||
|
int rowIndex = adjustedPageIndex / cols;
|
||||||
|
int colIndex = adjustedPageIndex % cols;
|
||||||
|
|
||||||
|
if (destinationPageIndex >= newDocument.getNumberOfPages()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
PDPage destinationPage = newDocument.getPage(destinationPageIndex);
|
||||||
|
PDRectangle sourceRect = sourcePage.getMediaBox();
|
||||||
|
|
||||||
|
float scaleWidth = cellWidth / sourceRect.getWidth();
|
||||||
|
float scaleHeight = cellHeight / sourceRect.getHeight();
|
||||||
|
float scale = Math.min(scaleWidth, scaleHeight);
|
||||||
|
|
||||||
|
float x = colIndex * cellWidth + (cellWidth - sourceRect.getWidth() * scale) / 2;
|
||||||
|
float y =
|
||||||
|
destinationPage.getMediaBox().getHeight()
|
||||||
|
- ((rowIndex + 1) * cellHeight
|
||||||
|
- (cellHeight - sourceRect.getHeight() * scale) / 2);
|
||||||
|
|
||||||
|
copyBasicFormFields(
|
||||||
|
sourceAcroForm,
|
||||||
|
newAcroForm,
|
||||||
|
sourcePage,
|
||||||
|
destinationPage,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
scale,
|
||||||
|
pageIndex,
|
||||||
|
fieldNameCounters,
|
||||||
|
widgetFieldMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh appearances to ensure widgets render correctly across viewers
|
||||||
|
try {
|
||||||
|
// Use reflection to avoid compile-time dependency on PDFBox version
|
||||||
|
Method m = newAcroForm.getClass().getMethod("refreshAppearances");
|
||||||
|
m.invoke(newAcroForm);
|
||||||
|
} catch (NoSuchMethodException nsme) {
|
||||||
|
log.warn(
|
||||||
|
"AcroForm.refreshAppearances() not available in this PDFBox version; relying on NeedAppearances.");
|
||||||
|
} catch (Throwable t) {
|
||||||
|
log.warn("Failed to refresh field appearances via AcroForm: {}", t.getMessage(), t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void copyBasicFormFields(
|
||||||
|
PDAcroForm sourceAcroForm,
|
||||||
|
PDAcroForm newAcroForm,
|
||||||
|
PDPage sourcePage,
|
||||||
|
PDPage destinationPage,
|
||||||
|
float offsetX,
|
||||||
|
float offsetY,
|
||||||
|
float scale,
|
||||||
|
int pageIndex,
|
||||||
|
Map<String, Integer> fieldNameCounters,
|
||||||
|
Map<PDAnnotationWidget, PDField> widgetFieldMap) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
List<PDAnnotation> sourceAnnotations = sourcePage.getAnnotations();
|
||||||
|
List<PDAnnotation> destinationAnnotations = destinationPage.getAnnotations();
|
||||||
|
|
||||||
|
for (PDAnnotation annotation : sourceAnnotations) {
|
||||||
|
if (annotation instanceof PDAnnotationWidget widgetAnnotation) {
|
||||||
|
if (widgetAnnotation.getRectangle() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
PDField sourceField =
|
||||||
|
widgetFieldMap != null ? widgetFieldMap.get(widgetAnnotation) : null;
|
||||||
|
if (sourceField == null) {
|
||||||
|
continue; // skip widgets without a matching field
|
||||||
|
}
|
||||||
|
if (sourceField instanceof PDTextField pdtextfield) {
|
||||||
|
createSimpleTextField(
|
||||||
|
newAcroForm,
|
||||||
|
destinationPage,
|
||||||
|
destinationAnnotations,
|
||||||
|
pdtextfield,
|
||||||
|
widgetAnnotation,
|
||||||
|
offsetX,
|
||||||
|
offsetY,
|
||||||
|
scale,
|
||||||
|
pageIndex,
|
||||||
|
fieldNameCounters);
|
||||||
|
} else if (sourceField instanceof PDCheckBox pdCheckBox) {
|
||||||
|
createSimpleCheckBoxField(
|
||||||
|
newAcroForm,
|
||||||
|
destinationPage,
|
||||||
|
destinationAnnotations,
|
||||||
|
pdCheckBox,
|
||||||
|
widgetAnnotation,
|
||||||
|
offsetX,
|
||||||
|
offsetY,
|
||||||
|
scale,
|
||||||
|
pageIndex,
|
||||||
|
fieldNameCounters);
|
||||||
|
} else if (sourceField instanceof PDRadioButton pdRadioButton) {
|
||||||
|
createSimpleRadioButtonField(
|
||||||
|
newAcroForm,
|
||||||
|
destinationPage,
|
||||||
|
destinationAnnotations,
|
||||||
|
pdRadioButton,
|
||||||
|
widgetAnnotation,
|
||||||
|
offsetX,
|
||||||
|
offsetY,
|
||||||
|
scale,
|
||||||
|
pageIndex,
|
||||||
|
fieldNameCounters);
|
||||||
|
} else if (sourceField instanceof PDComboBox pdComboBox) {
|
||||||
|
createSimpleComboBoxField(
|
||||||
|
newAcroForm,
|
||||||
|
destinationPage,
|
||||||
|
destinationAnnotations,
|
||||||
|
pdComboBox,
|
||||||
|
widgetAnnotation,
|
||||||
|
offsetX,
|
||||||
|
offsetY,
|
||||||
|
scale,
|
||||||
|
pageIndex,
|
||||||
|
fieldNameCounters);
|
||||||
|
} else if (sourceField instanceof PDListBox pdlistbox) {
|
||||||
|
createSimpleListBoxField(
|
||||||
|
newAcroForm,
|
||||||
|
destinationPage,
|
||||||
|
destinationAnnotations,
|
||||||
|
pdlistbox,
|
||||||
|
widgetAnnotation,
|
||||||
|
offsetX,
|
||||||
|
offsetY,
|
||||||
|
scale,
|
||||||
|
pageIndex,
|
||||||
|
fieldNameCounters);
|
||||||
|
} else if (sourceField instanceof PDSignatureField pdSignatureField) {
|
||||||
|
createSimpleSignatureField(
|
||||||
|
newAcroForm,
|
||||||
|
destinationPage,
|
||||||
|
destinationAnnotations,
|
||||||
|
pdSignatureField,
|
||||||
|
widgetAnnotation,
|
||||||
|
offsetX,
|
||||||
|
offsetY,
|
||||||
|
scale,
|
||||||
|
pageIndex,
|
||||||
|
fieldNameCounters);
|
||||||
|
} else if (sourceField instanceof PDPushButton pdPushButton) {
|
||||||
|
createSimplePushButtonField(
|
||||||
|
newAcroForm,
|
||||||
|
destinationPage,
|
||||||
|
destinationAnnotations,
|
||||||
|
pdPushButton,
|
||||||
|
widgetAnnotation,
|
||||||
|
offsetX,
|
||||||
|
offsetY,
|
||||||
|
scale,
|
||||||
|
pageIndex,
|
||||||
|
fieldNameCounters);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn(
|
||||||
|
"Failed to copy basic form fields for page {}: {}",
|
||||||
|
pageIndex,
|
||||||
|
e.getMessage(),
|
||||||
|
e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void createSimpleTextField(
|
||||||
|
PDAcroForm newAcroForm,
|
||||||
|
PDPage destinationPage,
|
||||||
|
List<PDAnnotation> destinationAnnotations,
|
||||||
|
PDTextField sourceField,
|
||||||
|
PDAnnotationWidget sourceWidget,
|
||||||
|
float offsetX,
|
||||||
|
float offsetY,
|
||||||
|
float scale,
|
||||||
|
int pageIndex,
|
||||||
|
Map<String, Integer> fieldNameCounters) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
PDTextField newTextField = new PDTextField(newAcroForm);
|
||||||
|
newTextField.setDefaultAppearance("/Helv 12 Tf 0 g");
|
||||||
|
|
||||||
|
boolean initialized =
|
||||||
|
initializeFieldWithWidget(
|
||||||
|
newAcroForm,
|
||||||
|
destinationPage,
|
||||||
|
destinationAnnotations,
|
||||||
|
newTextField,
|
||||||
|
sourceField.getPartialName(),
|
||||||
|
"textField",
|
||||||
|
sourceWidget,
|
||||||
|
offsetX,
|
||||||
|
offsetY,
|
||||||
|
scale,
|
||||||
|
pageIndex,
|
||||||
|
fieldNameCounters);
|
||||||
|
|
||||||
|
if (!initialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sourceField.getValueAsString() != null) {
|
||||||
|
newTextField.setValue(sourceField.getValueAsString());
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn(
|
||||||
|
"Failed to create text field '{}': {}",
|
||||||
|
sourceField.getPartialName(),
|
||||||
|
e.getMessage(),
|
||||||
|
e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void createSimpleCheckBoxField(
|
||||||
|
PDAcroForm newAcroForm,
|
||||||
|
PDPage destinationPage,
|
||||||
|
List<PDAnnotation> destinationAnnotations,
|
||||||
|
PDCheckBox sourceField,
|
||||||
|
PDAnnotationWidget sourceWidget,
|
||||||
|
float offsetX,
|
||||||
|
float offsetY,
|
||||||
|
float scale,
|
||||||
|
int pageIndex,
|
||||||
|
Map<String, Integer> fieldNameCounters) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
PDCheckBox newCheckBox = new PDCheckBox(newAcroForm);
|
||||||
|
|
||||||
|
boolean initialized =
|
||||||
|
initializeFieldWithWidget(
|
||||||
|
newAcroForm,
|
||||||
|
destinationPage,
|
||||||
|
destinationAnnotations,
|
||||||
|
newCheckBox,
|
||||||
|
sourceField.getPartialName(),
|
||||||
|
"checkBox",
|
||||||
|
sourceWidget,
|
||||||
|
offsetX,
|
||||||
|
offsetY,
|
||||||
|
scale,
|
||||||
|
pageIndex,
|
||||||
|
fieldNameCounters);
|
||||||
|
|
||||||
|
if (!initialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sourceField.isChecked()) {
|
||||||
|
newCheckBox.check();
|
||||||
|
} else {
|
||||||
|
newCheckBox.unCheck();
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn(
|
||||||
|
"Failed to create checkbox field '{}': {}",
|
||||||
|
sourceField.getPartialName(),
|
||||||
|
e.getMessage(),
|
||||||
|
e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void createSimpleRadioButtonField(
|
||||||
|
PDAcroForm newAcroForm,
|
||||||
|
PDPage destinationPage,
|
||||||
|
List<PDAnnotation> destinationAnnotations,
|
||||||
|
PDRadioButton sourceField,
|
||||||
|
PDAnnotationWidget sourceWidget,
|
||||||
|
float offsetX,
|
||||||
|
float offsetY,
|
||||||
|
float scale,
|
||||||
|
int pageIndex,
|
||||||
|
Map<String, Integer> fieldNameCounters) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
PDRadioButton newRadioButton = new PDRadioButton(newAcroForm);
|
||||||
|
|
||||||
|
boolean initialized =
|
||||||
|
initializeFieldWithWidget(
|
||||||
|
newAcroForm,
|
||||||
|
destinationPage,
|
||||||
|
destinationAnnotations,
|
||||||
|
newRadioButton,
|
||||||
|
sourceField.getPartialName(),
|
||||||
|
"radioButton",
|
||||||
|
sourceWidget,
|
||||||
|
offsetX,
|
||||||
|
offsetY,
|
||||||
|
scale,
|
||||||
|
pageIndex,
|
||||||
|
fieldNameCounters);
|
||||||
|
|
||||||
|
if (!initialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sourceField.getExportValues() != null) {
|
||||||
|
newRadioButton.setExportValues(sourceField.getExportValues());
|
||||||
|
}
|
||||||
|
if (sourceField.getValue() != null) {
|
||||||
|
newRadioButton.setValue(sourceField.getValue());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn(
|
||||||
|
"Failed to create radio button field '{}': {}",
|
||||||
|
sourceField.getPartialName(),
|
||||||
|
e.getMessage(),
|
||||||
|
e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void createSimpleComboBoxField(
|
||||||
|
PDAcroForm newAcroForm,
|
||||||
|
PDPage destinationPage,
|
||||||
|
List<PDAnnotation> destinationAnnotations,
|
||||||
|
PDComboBox sourceField,
|
||||||
|
PDAnnotationWidget sourceWidget,
|
||||||
|
float offsetX,
|
||||||
|
float offsetY,
|
||||||
|
float scale,
|
||||||
|
int pageIndex,
|
||||||
|
Map<String, Integer> fieldNameCounters) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
PDComboBox newComboBox = new PDComboBox(newAcroForm);
|
||||||
|
|
||||||
|
boolean initialized =
|
||||||
|
initializeFieldWithWidget(
|
||||||
|
newAcroForm,
|
||||||
|
destinationPage,
|
||||||
|
destinationAnnotations,
|
||||||
|
newComboBox,
|
||||||
|
sourceField.getPartialName(),
|
||||||
|
"comboBox",
|
||||||
|
sourceWidget,
|
||||||
|
offsetX,
|
||||||
|
offsetY,
|
||||||
|
scale,
|
||||||
|
pageIndex,
|
||||||
|
fieldNameCounters);
|
||||||
|
|
||||||
|
if (!initialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sourceField.getOptions() != null) {
|
||||||
|
newComboBox.setOptions(sourceField.getOptions());
|
||||||
|
}
|
||||||
|
if (sourceField.getValue() != null && !sourceField.getValue().isEmpty()) {
|
||||||
|
newComboBox.setValue(sourceField.getValue());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn(
|
||||||
|
"Failed to create combo box field '{}': {}",
|
||||||
|
sourceField.getPartialName(),
|
||||||
|
e.getMessage(),
|
||||||
|
e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void createSimpleListBoxField(
|
||||||
|
PDAcroForm newAcroForm,
|
||||||
|
PDPage destinationPage,
|
||||||
|
List<PDAnnotation> destinationAnnotations,
|
||||||
|
PDListBox sourceField,
|
||||||
|
PDAnnotationWidget sourceWidget,
|
||||||
|
float offsetX,
|
||||||
|
float offsetY,
|
||||||
|
float scale,
|
||||||
|
int pageIndex,
|
||||||
|
Map<String, Integer> fieldNameCounters) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
PDListBox newListBox = new PDListBox(newAcroForm);
|
||||||
|
|
||||||
|
boolean initialized =
|
||||||
|
initializeFieldWithWidget(
|
||||||
|
newAcroForm,
|
||||||
|
destinationPage,
|
||||||
|
destinationAnnotations,
|
||||||
|
newListBox,
|
||||||
|
sourceField.getPartialName(),
|
||||||
|
"listBox",
|
||||||
|
sourceWidget,
|
||||||
|
offsetX,
|
||||||
|
offsetY,
|
||||||
|
scale,
|
||||||
|
pageIndex,
|
||||||
|
fieldNameCounters);
|
||||||
|
|
||||||
|
if (!initialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sourceField.getOptions() != null) {
|
||||||
|
newListBox.setOptions(sourceField.getOptions());
|
||||||
|
}
|
||||||
|
if (sourceField.getValue() != null && !sourceField.getValue().isEmpty()) {
|
||||||
|
newListBox.setValue(sourceField.getValue());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn(
|
||||||
|
"Failed to create list box field '{}': {}",
|
||||||
|
sourceField.getPartialName(),
|
||||||
|
e.getMessage(),
|
||||||
|
e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void createSimpleSignatureField(
|
||||||
|
PDAcroForm newAcroForm,
|
||||||
|
PDPage destinationPage,
|
||||||
|
List<PDAnnotation> destinationAnnotations,
|
||||||
|
PDSignatureField sourceField,
|
||||||
|
PDAnnotationWidget sourceWidget,
|
||||||
|
float offsetX,
|
||||||
|
float offsetY,
|
||||||
|
float scale,
|
||||||
|
int pageIndex,
|
||||||
|
Map<String, Integer> fieldNameCounters) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
PDSignatureField newSignatureField = new PDSignatureField(newAcroForm);
|
||||||
|
|
||||||
|
boolean initialized =
|
||||||
|
initializeFieldWithWidget(
|
||||||
|
newAcroForm,
|
||||||
|
destinationPage,
|
||||||
|
destinationAnnotations,
|
||||||
|
newSignatureField,
|
||||||
|
sourceField.getPartialName(),
|
||||||
|
"signature",
|
||||||
|
sourceWidget,
|
||||||
|
offsetX,
|
||||||
|
offsetY,
|
||||||
|
scale,
|
||||||
|
pageIndex,
|
||||||
|
fieldNameCounters);
|
||||||
|
|
||||||
|
if (!initialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn(
|
||||||
|
"Failed to create signature field '{}': {}",
|
||||||
|
sourceField.getPartialName(),
|
||||||
|
e.getMessage(),
|
||||||
|
e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void createSimplePushButtonField(
|
||||||
|
PDAcroForm newAcroForm,
|
||||||
|
PDPage destinationPage,
|
||||||
|
List<PDAnnotation> destinationAnnotations,
|
||||||
|
PDPushButton sourceField,
|
||||||
|
PDAnnotationWidget sourceWidget,
|
||||||
|
float offsetX,
|
||||||
|
float offsetY,
|
||||||
|
float scale,
|
||||||
|
int pageIndex,
|
||||||
|
Map<String, Integer> fieldNameCounters) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
PDPushButton newPushButton = new PDPushButton(newAcroForm);
|
||||||
|
|
||||||
|
boolean initialized =
|
||||||
|
initializeFieldWithWidget(
|
||||||
|
newAcroForm,
|
||||||
|
destinationPage,
|
||||||
|
destinationAnnotations,
|
||||||
|
newPushButton,
|
||||||
|
sourceField.getPartialName(),
|
||||||
|
"pushButton",
|
||||||
|
sourceWidget,
|
||||||
|
offsetX,
|
||||||
|
offsetY,
|
||||||
|
scale,
|
||||||
|
pageIndex,
|
||||||
|
fieldNameCounters);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn(
|
||||||
|
"Failed to create push button field '{}': {}",
|
||||||
|
sourceField.getPartialName(),
|
||||||
|
e.getMessage(),
|
||||||
|
e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T extends PDTerminalField> boolean initializeFieldWithWidget(
|
||||||
|
PDAcroForm newAcroForm,
|
||||||
|
PDPage destinationPage,
|
||||||
|
List<PDAnnotation> destinationAnnotations,
|
||||||
|
T newField,
|
||||||
|
String originalName,
|
||||||
|
String fallbackName,
|
||||||
|
PDAnnotationWidget sourceWidget,
|
||||||
|
float offsetX,
|
||||||
|
float offsetY,
|
||||||
|
float scale,
|
||||||
|
int pageIndex,
|
||||||
|
Map<String, Integer> fieldNameCounters) {
|
||||||
|
|
||||||
|
String baseName = (originalName != null) ? originalName : fallbackName;
|
||||||
|
String newFieldName = generateUniqueFieldName(baseName, pageIndex, fieldNameCounters);
|
||||||
|
newField.setPartialName(newFieldName);
|
||||||
|
|
||||||
|
PDAnnotationWidget newWidget = new PDAnnotationWidget();
|
||||||
|
PDRectangle sourceRect = sourceWidget.getRectangle();
|
||||||
|
if (sourceRect == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
float newX = (sourceRect.getLowerLeftX() * scale) + offsetX;
|
||||||
|
float newY = (sourceRect.getLowerLeftY() * scale) + offsetY;
|
||||||
|
float newWidth = sourceRect.getWidth() * scale;
|
||||||
|
float newHeight = sourceRect.getHeight() * scale;
|
||||||
|
newWidget.setRectangle(new PDRectangle(newX, newY, newWidth, newHeight));
|
||||||
|
newWidget.setPage(destinationPage);
|
||||||
|
|
||||||
|
newField.getWidgets().add(newWidget);
|
||||||
|
newWidget.setParent(newField);
|
||||||
|
newAcroForm.getFields().add(newField);
|
||||||
|
destinationAnnotations.add(newWidget);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String generateUniqueFieldName(
|
||||||
|
String originalName, int pageIndex, Map<String, Integer> fieldNameCounters) {
|
||||||
|
String baseName = "page" + pageIndex + "_" + originalName;
|
||||||
|
|
||||||
|
Integer counter = fieldNameCounters.get(baseName);
|
||||||
|
if (counter == null) {
|
||||||
|
counter = 0;
|
||||||
|
} else {
|
||||||
|
counter++;
|
||||||
|
}
|
||||||
|
fieldNameCounters.put(baseName, counter);
|
||||||
|
|
||||||
|
return counter == 0 ? baseName : baseName + "_" + counter;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<PDAnnotationWidget, PDField> buildWidgetFieldMap(PDAcroForm acroForm) {
|
||||||
|
Map<PDAnnotationWidget, PDField> map = new HashMap<>();
|
||||||
|
if (acroForm == null) {
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
for (PDField field : acroForm.getFieldTree()) {
|
||||||
|
List<PDAnnotationWidget> widgets = field.getWidgets();
|
||||||
|
if (widgets != null) {
|
||||||
|
for (PDAnnotationWidget w : widgets) {
|
||||||
|
if (w != null) {
|
||||||
|
map.put(w, field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Failed to build widget->field map: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,13 +9,9 @@ import java.nio.charset.StandardCharsets;
|
|||||||
import java.nio.file.*;
|
import java.nio.file.*;
|
||||||
import java.nio.file.attribute.BasicFileAttributes;
|
import java.nio.file.attribute.BasicFileAttributes;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.util.ArrayList;
|
import java.util.*;
|
||||||
import java.util.Arrays;
|
import java.util.regex.Matcher;
|
||||||
import java.util.Enumeration;
|
import java.util.regex.Pattern;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import org.springframework.core.io.ClassPathResource;
|
import org.springframework.core.io.ClassPathResource;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
@@ -28,25 +24,37 @@ import com.fathzer.soft.javaluator.DoubleEvaluator;
|
|||||||
import io.github.pixee.security.HostValidator;
|
import io.github.pixee.security.HostValidator;
|
||||||
import io.github.pixee.security.Urls;
|
import io.github.pixee.security.Urls;
|
||||||
|
|
||||||
|
import lombok.experimental.UtilityClass;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.common.configuration.InstallationPathConfig;
|
import stirling.software.common.configuration.InstallationPathConfig;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
@UtilityClass
|
||||||
public class GeneralUtils {
|
public class GeneralUtils {
|
||||||
|
|
||||||
private static final Set<String> DEFAULT_VALID_SCRIPTS =
|
private final Set<String> DEFAULT_VALID_SCRIPTS = Set.of("png_to_webp.py", "split_photos.py");
|
||||||
Set.of("png_to_webp.py", "split_photos.py");
|
private final Set<String> DEFAULT_VALID_PIPELINE =
|
||||||
private static final Set<String> DEFAULT_VALID_PIPELINE =
|
|
||||||
Set.of(
|
Set.of(
|
||||||
"OCR images.json",
|
"OCR images.json",
|
||||||
"Prepare-pdfs-for-email.json",
|
"Prepare-pdfs-for-email.json",
|
||||||
"split-rotate-auto-rename.json");
|
"split-rotate-auto-rename.json");
|
||||||
|
|
||||||
private static final String DEFAULT_WEBUI_CONFIGS_DIR = "defaultWebUIConfigs";
|
private final String DEFAULT_WEBUI_CONFIGS_DIR = "defaultWebUIConfigs";
|
||||||
private static final String PYTHON_SCRIPTS_DIR = "python";
|
private final String PYTHON_SCRIPTS_DIR = "python";
|
||||||
|
private final RegexPatternUtils patternCache = RegexPatternUtils.getInstance();
|
||||||
|
// Valid size units used for convertSizeToBytes validation and parsing
|
||||||
|
private final Set<String> VALID_SIZE_UNITS = Set.of("B", "KB", "MB", "GB", "TB");
|
||||||
|
|
||||||
public static File convertMultipartFileToFile(MultipartFile multipartFile) throws IOException {
|
/*
|
||||||
|
* Converts a MultipartFile to a regular File with improved performance and security.
|
||||||
|
*
|
||||||
|
* @param multipartFile the multipart file to convert
|
||||||
|
* @return temporary File containing the multipart file data
|
||||||
|
* @throws IOException if I/O error occurs during conversion
|
||||||
|
* @throws IllegalArgumentException if file exceeds maximum allowed size
|
||||||
|
*/
|
||||||
|
public File convertMultipartFileToFile(MultipartFile multipartFile) throws IOException {
|
||||||
String customTempDir = System.getenv("STIRLING_TEMPFILES_DIRECTORY");
|
String customTempDir = System.getenv("STIRLING_TEMPFILES_DIRECTORY");
|
||||||
if (customTempDir == null || customTempDir.isEmpty()) {
|
if (customTempDir == null || customTempDir.isEmpty()) {
|
||||||
customTempDir = System.getProperty("stirling.tempfiles.directory");
|
customTempDir = System.getProperty("stirling.tempfiles.directory");
|
||||||
@@ -81,10 +89,137 @@ public class GeneralUtils {
|
|||||||
return tempFile;
|
return tempFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void deleteDirectory(Path path) throws IOException {
|
/*
|
||||||
|
* Gets the configured temporary directory, creating it if necessary.
|
||||||
|
*
|
||||||
|
* @return Path to the temporary directory
|
||||||
|
* @throws IOException if directory creation fails
|
||||||
|
*/
|
||||||
|
private Path getTempDirectory() throws IOException {
|
||||||
|
String customTempDir = System.getenv("STIRLING_TEMPFILES_DIRECTORY");
|
||||||
|
if (customTempDir == null || customTempDir.isEmpty()) {
|
||||||
|
customTempDir = System.getProperty("stirling.tempfiles.directory");
|
||||||
|
}
|
||||||
|
|
||||||
|
Path tempDir;
|
||||||
|
if (customTempDir != null && !customTempDir.isEmpty()) {
|
||||||
|
tempDir = Path.of(customTempDir);
|
||||||
|
} else {
|
||||||
|
tempDir = Path.of(System.getProperty("java.io.tmpdir"), "stirling-pdf");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Files.exists(tempDir)) {
|
||||||
|
Files.createDirectories(tempDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tempDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Remove file extension
|
||||||
|
*
|
||||||
|
* <p>Uses fast string operations for common cases (valid extensions) and falls back to
|
||||||
|
* optimized regex for edge cases (no extension, hidden files, etc.).
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>String operations avoid regex engine overhead for common cases
|
||||||
|
* <li>Cached pattern compilation eliminates recompilation costs
|
||||||
|
* <li>Fresh Matcher instances ensure thread safety
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @param filename the filename to process, may be null
|
||||||
|
* @return filename without extension, or "default" if input is null
|
||||||
|
*/
|
||||||
|
public String removeExtension(String filename) {
|
||||||
|
if (filename == null) {
|
||||||
|
return "default";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filename.isEmpty()) {
|
||||||
|
return filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
int dotIndex = filename.lastIndexOf('.');
|
||||||
|
if (dotIndex > 0 && dotIndex < filename.length() - 1) {
|
||||||
|
return filename.substring(0, dotIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dotIndex == 0 || dotIndex == filename.length() - 1 || dotIndex == -1) {
|
||||||
|
return filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
Pattern pattern = patternCache.getPattern(RegexPatternUtils.getExtensionRegex());
|
||||||
|
Matcher matcher = pattern.matcher(filename);
|
||||||
|
return matcher.find() ? matcher.replaceFirst("") : filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Append suffix to base name with null safety.
|
||||||
|
*
|
||||||
|
* @param baseName the base filename, null becomes "default"
|
||||||
|
* @param suffix the suffix to append, null becomes empty string
|
||||||
|
* @return concatenated string with null safety
|
||||||
|
*/
|
||||||
|
public String appendSuffix(String baseName, String suffix) {
|
||||||
|
return (baseName == null ? "default" : baseName) + (suffix != null ? suffix : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Generate a PDF filename by removing extension from first file and adding suffix.
|
||||||
|
*
|
||||||
|
* <p>High-level utility method for common PDF naming scenarios. Handles null safety and uses
|
||||||
|
* extension removal.
|
||||||
|
*
|
||||||
|
* @param firstFilename the filename of the first file being, may be null
|
||||||
|
* @param suffix the suffix to append (e.g., "_merged.pdf")
|
||||||
|
* @return filename with suffix, or default name if input is null
|
||||||
|
*/
|
||||||
|
public String generateFilename(String firstFilename, String suffix) {
|
||||||
|
String baseName = removeExtension(firstFilename);
|
||||||
|
return appendSuffix(baseName, suffix);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Process a list of filenames by removing extensions and adding suffix.
|
||||||
|
*
|
||||||
|
* <p>Efficiently processes multiple filenames using streaming operations and bulk operations
|
||||||
|
* where possible. Handles null safety for both input list and individual filenames.
|
||||||
|
*
|
||||||
|
* @param filenames the list of filenames to process, may be null
|
||||||
|
* @param suffix the suffix to append to each processed filename
|
||||||
|
* @param processor consumer to handle each processed filename, may be null
|
||||||
|
*/
|
||||||
|
public void processFilenames(
|
||||||
|
List<String> filenames, String suffix, java.util.function.Consumer<String> processor) {
|
||||||
|
if (filenames == null || processor == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
filenames.stream()
|
||||||
|
.map(filename -> appendSuffix(removeExtension(filename), suffix))
|
||||||
|
.forEach(processor);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Extract title from filename by removing extension, with fallback handling.
|
||||||
|
*
|
||||||
|
* <p>Returns "Untitled" for null or empty filenames, otherwise removes the extension using the
|
||||||
|
* optimized removeExtension method.
|
||||||
|
*
|
||||||
|
* @param filename the filename to extract title from, may be null
|
||||||
|
* @return the title without extension, or "Untitled" if input is null/empty
|
||||||
|
*/
|
||||||
|
public String getTitleFromFilename(String filename) {
|
||||||
|
if (filename == null || filename.isEmpty()) {
|
||||||
|
return "Untitled";
|
||||||
|
}
|
||||||
|
return removeExtension(filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteDirectory(Path path) throws IOException {
|
||||||
Files.walkFileTree(
|
Files.walkFileTree(
|
||||||
path,
|
path,
|
||||||
new SimpleFileVisitor<Path>() {
|
new SimpleFileVisitor<>() {
|
||||||
@Override
|
@Override
|
||||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
|
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
@@ -101,8 +236,18 @@ public class GeneralUtils {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String convertToFileName(String name) {
|
public String convertToFileName(String name) {
|
||||||
String safeName = name.replaceAll("[^a-zA-Z0-9]", "_");
|
if (name == null) return "_";
|
||||||
|
StringBuilder safeNameBuilder = new StringBuilder(name.length());
|
||||||
|
for (int i = 0; i < name.length(); i++) {
|
||||||
|
char c = name.charAt(i);
|
||||||
|
if (Character.isLetterOrDigit(c)) {
|
||||||
|
safeNameBuilder.append(c);
|
||||||
|
} else {
|
||||||
|
safeNameBuilder.append('_');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String safeName = safeNameBuilder.toString();
|
||||||
if (safeName.length() > 50) {
|
if (safeName.length() > 50) {
|
||||||
safeName = safeName.substring(0, 50);
|
safeName = safeName.substring(0, 50);
|
||||||
}
|
}
|
||||||
@@ -110,19 +255,20 @@ public class GeneralUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get resources from a location pattern
|
// Get resources from a location pattern
|
||||||
public static Resource[] getResourcesFromLocationPattern(
|
public Resource[] getResourcesFromLocationPattern(
|
||||||
String locationPattern, ResourceLoader resourceLoader) throws Exception {
|
String locationPattern, ResourceLoader resourceLoader) throws Exception {
|
||||||
// Normalize the path for file resources
|
// Normalize the path for file resources
|
||||||
if (locationPattern.startsWith("file:")) {
|
String pattern = locationPattern;
|
||||||
String rawPath = locationPattern.substring(5).replace("\\*", "").replace("/*", "");
|
if (pattern.startsWith("file:")) {
|
||||||
|
String rawPath = pattern.substring(5).replace("\\*", "").replace("/*", "");
|
||||||
Path normalizePath = Paths.get(rawPath).normalize();
|
Path normalizePath = Paths.get(rawPath).normalize();
|
||||||
locationPattern = "file:" + normalizePath.toString().replace("\\", "/") + "/*";
|
pattern = "file:" + normalizePath.toString().replace("\\", "/") + "/*";
|
||||||
}
|
}
|
||||||
return ResourcePatternUtils.getResourcePatternResolver(resourceLoader)
|
return ResourcePatternUtils.getResourcePatternResolver(resourceLoader)
|
||||||
.getResources(locationPattern);
|
.getResources(pattern);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isValidURL(String urlStr) {
|
public boolean isValidURL(String urlStr) {
|
||||||
try {
|
try {
|
||||||
Urls.create(
|
Urls.create(
|
||||||
urlStr, Urls.HTTP_PROTOCOLS, HostValidator.DENY_COMMON_INFRASTRUCTURE_TARGETS);
|
urlStr, Urls.HTTP_PROTOCOLS, HostValidator.DENY_COMMON_INFRASTRUCTURE_TARGETS);
|
||||||
@@ -132,7 +278,25 @@ public class GeneralUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isURLReachable(String urlStr) {
|
/*
|
||||||
|
* Checks if a URL is reachable with proper timeout configuration and error handling.
|
||||||
|
*
|
||||||
|
* @param urlStr the URL string to check
|
||||||
|
* @return true if URL is reachable, false otherwise
|
||||||
|
*/
|
||||||
|
public boolean isURLReachable(String urlStr) {
|
||||||
|
return isURLReachable(urlStr, 5000, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Checks if a URL is reachable with configurable timeouts.
|
||||||
|
*
|
||||||
|
* @param urlStr the URL string to check
|
||||||
|
* @param connectTimeout connection timeout in milliseconds
|
||||||
|
* @param readTimeout read timeout in milliseconds
|
||||||
|
* @return true if URL is reachable, false otherwise
|
||||||
|
*/
|
||||||
|
public boolean isURLReachable(String urlStr, int connectTimeout, int readTimeout) {
|
||||||
try {
|
try {
|
||||||
// Parse the URL
|
// Parse the URL
|
||||||
URL url = URI.create(urlStr).toURL();
|
URL url = URI.create(urlStr).toURL();
|
||||||
@@ -152,16 +316,19 @@ public class GeneralUtils {
|
|||||||
// Check if the URL is reachable
|
// Check if the URL is reachable
|
||||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||||
connection.setRequestMethod("HEAD");
|
connection.setRequestMethod("HEAD");
|
||||||
// connection.setConnectTimeout(5000); // Set connection timeout
|
connection.setConnectTimeout(connectTimeout);
|
||||||
// connection.setReadTimeout(5000); // Set read timeout
|
connection.setReadTimeout(readTimeout);
|
||||||
|
connection.setInstanceFollowRedirects(false); // Security: prevent redirect loops
|
||||||
|
|
||||||
int responseCode = connection.getResponseCode();
|
int responseCode = connection.getResponseCode();
|
||||||
return (200 <= responseCode && responseCode <= 399);
|
return (200 <= responseCode && responseCode <= 399);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
log.debug("URL {} is not reachable: {}", urlStr, e.getMessage());
|
||||||
return false; // Return false in case of any exception
|
return false; // Return false in case of any exception
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isLocalAddress(String host) {
|
private boolean isLocalAddress(String host) {
|
||||||
try {
|
try {
|
||||||
// Resolve DNS to IP address
|
// Resolve DNS to IP address
|
||||||
InetAddress address = InetAddress.getByName(host);
|
InetAddress address = InetAddress.getByName(host);
|
||||||
@@ -181,7 +348,14 @@ public class GeneralUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static File multipartToFile(MultipartFile multipart) throws IOException {
|
/*
|
||||||
|
* Improved multipart file conversion using the shared helper method.
|
||||||
|
*
|
||||||
|
* @param multipart the multipart file to convert
|
||||||
|
* @return temporary File containing the multipart file data
|
||||||
|
* @throws IOException if I/O error occurs during conversion
|
||||||
|
*/
|
||||||
|
public File multipartToFile(MultipartFile multipart) throws IOException {
|
||||||
Path tempFile = Files.createTempFile("overlay-", ".pdf");
|
Path tempFile = Files.createTempFile("overlay-", ".pdf");
|
||||||
try (InputStream in = multipart.getInputStream();
|
try (InputStream in = multipart.getInputStream();
|
||||||
FileOutputStream out = new FileOutputStream(tempFile.toFile())) {
|
FileOutputStream out = new FileOutputStream(tempFile.toFile())) {
|
||||||
@@ -194,54 +368,105 @@ public class GeneralUtils {
|
|||||||
return tempFile.toFile();
|
return tempFile.toFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Long convertSizeToBytes(String sizeStr) {
|
/*
|
||||||
|
* Supports TB/PB units and provides detailed error messages.
|
||||||
|
*
|
||||||
|
* @param sizeStr the size string to convert (e.g., "100MB", "1.5GB")
|
||||||
|
* @param defaultUnit the default unit to assume if none specified ("MB", "GB", etc.)
|
||||||
|
* @return size in bytes, or null if parsing fails
|
||||||
|
* @throws IllegalArgumentException if defaultUnit is invalid
|
||||||
|
*/
|
||||||
|
public Long convertSizeToBytes(String sizeStr, String defaultUnit) {
|
||||||
if (sizeStr == null) {
|
if (sizeStr == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (defaultUnit != null && !isValidSizeUnit(defaultUnit)) {
|
||||||
|
throw new IllegalArgumentException("Invalid default unit: " + defaultUnit);
|
||||||
|
}
|
||||||
|
|
||||||
sizeStr = sizeStr.trim().toUpperCase();
|
sizeStr = sizeStr.trim().toUpperCase();
|
||||||
sizeStr = sizeStr.replace(",", ".").replace(" ", "");
|
sizeStr = sizeStr.replace(",", ".").replace(" ", "");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (sizeStr.endsWith("KB")) {
|
if (sizeStr.endsWith("TB")) {
|
||||||
return (long)
|
|
||||||
(Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) * 1024);
|
|
||||||
} else if (sizeStr.endsWith("MB")) {
|
|
||||||
return (long)
|
return (long)
|
||||||
(Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2))
|
(Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2))
|
||||||
* 1024
|
* 1024L
|
||||||
* 1024);
|
* 1024L
|
||||||
|
* 1024L
|
||||||
|
* 1024L);
|
||||||
} else if (sizeStr.endsWith("GB")) {
|
} else if (sizeStr.endsWith("GB")) {
|
||||||
return (long)
|
return (long)
|
||||||
(Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2))
|
(Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2))
|
||||||
* 1024
|
* 1024L
|
||||||
* 1024
|
* 1024L
|
||||||
* 1024);
|
* 1024L);
|
||||||
} else if (sizeStr.endsWith("B")) {
|
} else if (sizeStr.endsWith("MB")) {
|
||||||
|
return (long)
|
||||||
|
(Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2))
|
||||||
|
* 1024L
|
||||||
|
* 1024L);
|
||||||
|
} else if (sizeStr.endsWith("KB")) {
|
||||||
|
return (long)
|
||||||
|
(Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) * 1024L);
|
||||||
|
} else if (!sizeStr.isEmpty() && sizeStr.charAt(sizeStr.length() - 1) == 'B') {
|
||||||
return Long.parseLong(sizeStr.substring(0, sizeStr.length() - 1));
|
return Long.parseLong(sizeStr.substring(0, sizeStr.length() - 1));
|
||||||
} else {
|
} else {
|
||||||
// Assume MB if no unit is specified
|
// Use provided default unit or fall back to MB
|
||||||
return (long) (Double.parseDouble(sizeStr) * 1024 * 1024);
|
String unit = defaultUnit != null ? defaultUnit.toUpperCase() : "MB";
|
||||||
|
double value = Double.parseDouble(sizeStr);
|
||||||
|
return switch (unit) {
|
||||||
|
case "TB" -> (long) (value * 1024L * 1024L * 1024L * 1024L);
|
||||||
|
case "GB" -> (long) (value * 1024L * 1024L * 1024L);
|
||||||
|
case "MB" -> (long) (value * 1024L * 1024L);
|
||||||
|
case "KB" -> (long) (value * 1024L);
|
||||||
|
case "B" -> (long) value;
|
||||||
|
default -> (long) (value * 1024L * 1024L); // Default to MB
|
||||||
|
};
|
||||||
}
|
}
|
||||||
} catch (NumberFormatException e) {
|
} catch (NumberFormatException e) {
|
||||||
// The numeric part of the input string cannot be parsed, handle this case
|
log.warn("Failed to parse size string '{}': {}", sizeStr, e.getMessage());
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Converts size string to bytes using MB as default unit.
|
||||||
|
*
|
||||||
|
* @param sizeStr the size string to convert
|
||||||
|
* @return size in bytes, or null if parsing fails
|
||||||
|
*/
|
||||||
|
public Long convertSizeToBytes(String sizeStr) {
|
||||||
|
return convertSizeToBytes(sizeStr, "MB");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Validates if a string represents a valid size unit. */
|
||||||
|
private boolean isValidSizeUnit(String unit) {
|
||||||
|
// Use a precomputed Set for O(1) lookup, normalize using a locale-safe toUpperCase
|
||||||
|
return unit != null && VALID_SIZE_UNITS.contains(unit.toUpperCase(Locale.ROOT));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Enhanced byte formatting with TB/PB support and better precision. */
|
||||||
|
public String formatBytes(long bytes) {
|
||||||
|
if (bytes < 0) {
|
||||||
|
return "Invalid size";
|
||||||
|
}
|
||||||
|
|
||||||
public static String formatBytes(long bytes) {
|
|
||||||
if (bytes < 1024) {
|
if (bytes < 1024) {
|
||||||
return bytes + " B";
|
return bytes + " B";
|
||||||
} else if (bytes < 1024 * 1024) {
|
} else if (bytes < 1024L * 1024L) {
|
||||||
return String.format(Locale.US, "%.2f KB", bytes / 1024.0);
|
return String.format(Locale.US, "%.2f KB", bytes / 1024.0);
|
||||||
} else if (bytes < 1024 * 1024 * 1024) {
|
} else if (bytes < 1024L * 1024L * 1024L) {
|
||||||
return String.format(Locale.US, "%.2f MB", bytes / (1024.0 * 1024.0));
|
return String.format(Locale.US, "%.2f MB", bytes / (1024.0 * 1024.0));
|
||||||
} else {
|
} else if (bytes < 1024L * 1024L * 1024L * 1024L) {
|
||||||
return String.format(Locale.US, "%.2f GB", bytes / (1024.0 * 1024.0 * 1024.0));
|
return String.format(Locale.US, "%.2f GB", bytes / (1024.0 * 1024.0 * 1024.0));
|
||||||
|
} else {
|
||||||
|
return String.format(Locale.US, "%.2f TB", bytes / (1024.0 * 1024.0 * 1024.0 * 1024.0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<Integer> parsePageList(String pages, int totalPages, boolean oneBased) {
|
public List<Integer> parsePageList(String pages, int totalPages, boolean oneBased) {
|
||||||
if (pages == null) {
|
if (pages == null) {
|
||||||
return List.of(1); // Default to first page if input is null
|
return List.of(1); // Default to first page if input is null
|
||||||
}
|
}
|
||||||
@@ -252,11 +477,11 @@ public class GeneralUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<Integer> parsePageList(String[] pages, int totalPages) {
|
public List<Integer> parsePageList(String[] pages, int totalPages) {
|
||||||
return parsePageList(pages, totalPages, false);
|
return parsePageList(pages, totalPages, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<Integer> parsePageList(String[] pages, int totalPages, boolean oneBased) {
|
public List<Integer> parsePageList(String[] pages, int totalPages, boolean oneBased) {
|
||||||
List<Integer> result = new ArrayList<>();
|
List<Integer> result = new ArrayList<>();
|
||||||
int offset = oneBased ? 1 : 0;
|
int offset = oneBased ? 1 : 0;
|
||||||
for (String page : pages) {
|
for (String page : pages) {
|
||||||
@@ -278,33 +503,72 @@ public class GeneralUtils {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<Integer> evaluateNFunc(String expression, int maxValue) {
|
/*
|
||||||
|
* Enhanced mathematical expression evaluation with bounds checking and timeout protection.
|
||||||
|
*
|
||||||
|
* @param expression the mathematical expression containing 'n'
|
||||||
|
* @param maxValue the maximum value for 'n' and result bounds
|
||||||
|
* @return list of valid page numbers
|
||||||
|
* @throws IllegalArgumentException if expression is invalid or unsafe
|
||||||
|
*/
|
||||||
|
public List<Integer> evaluateNFunc(String expression, int maxValue) {
|
||||||
|
if (expression == null || expression.trim().isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("Expression cannot be null or empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxValue <= 0 || maxValue > 10000) {
|
||||||
|
throw new IllegalArgumentException("maxValue must be between 1 and 10000 for safety");
|
||||||
|
}
|
||||||
|
|
||||||
List<Integer> results = new ArrayList<>();
|
List<Integer> results = new ArrayList<>();
|
||||||
DoubleEvaluator evaluator = new DoubleEvaluator();
|
DoubleEvaluator evaluator = new DoubleEvaluator();
|
||||||
|
|
||||||
// Validate the expression
|
// Validate the expression format
|
||||||
if (!expression.matches("[0-9n+\\-*/() ]+")) {
|
if (!RegexPatternUtils.getInstance()
|
||||||
throw new IllegalArgumentException("Invalid expression");
|
.getMathExpressionPattern()
|
||||||
|
.matcher(expression.trim())
|
||||||
|
.matches()) {
|
||||||
|
throw new IllegalArgumentException("Invalid expression format: " + expression);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int n = 1; n <= maxValue; n++) {
|
for (int n = 1; n <= maxValue; n++) {
|
||||||
// Replace 'n' with the current value of n, correctly handling numbers before
|
try {
|
||||||
// 'n'
|
// Replace 'n' with the current value of n, correctly handling numbers before 'n'
|
||||||
String sanitizedExpression = sanitizeNFunction(expression, n);
|
String sanitizedExpression = sanitizeNFunction(expression.trim(), n);
|
||||||
Double result = evaluator.evaluate(sanitizedExpression);
|
Double result = evaluator.evaluate(sanitizedExpression);
|
||||||
|
|
||||||
// Check if the result is null or not within bounds
|
// Check if the result is null or not within bounds
|
||||||
if (result == null) break;
|
if (result == null || !Double.isFinite(result)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (result.intValue() > 0 && result.intValue() <= maxValue)
|
int intResult = result.intValue();
|
||||||
results.add(result.intValue());
|
if (intResult > 0 && intResult <= maxValue) {
|
||||||
|
results.add(intResult);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug(
|
||||||
|
"Failed to evaluate expression '{}' for n={}: {}",
|
||||||
|
expression,
|
||||||
|
n,
|
||||||
|
e.getMessage());
|
||||||
|
// Continue with next value instead of breaking
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String sanitizeNFunction(String expression, int nValue) {
|
private String sanitizeNFunction(String expression, int nValue) {
|
||||||
String sanitizedExpression = expression.replace(" ", "");
|
// Remove all spaces using a specialized character removal
|
||||||
|
StringBuilder sb = new StringBuilder(expression.length());
|
||||||
|
for (int i = 0; i < expression.length(); i++) {
|
||||||
|
char c = expression.charAt(i);
|
||||||
|
if (c != ' ') {
|
||||||
|
sb.append(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String sanitizedExpression = sb.toString();
|
||||||
String multiplyByOpeningRoundBracketPattern =
|
String multiplyByOpeningRoundBracketPattern =
|
||||||
"([0-9n)])\\("; // example: n(n-1), 9(n-1), (n-1)(n-2)
|
"([0-9n)])\\("; // example: n(n-1), 9(n-1), (n-1)(n-2)
|
||||||
sanitizedExpression =
|
sanitizedExpression =
|
||||||
@@ -319,23 +583,45 @@ public class GeneralUtils {
|
|||||||
return sanitizedExpression;
|
return sanitizedExpression;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String insertMultiplicationBeforeN(String expression, int nValue) {
|
private String insertMultiplicationBeforeN(String expression, int nValue) {
|
||||||
// Insert multiplication between a number and 'n' (e.g., "4n" becomes "4*n")
|
// Insert multiplication between a number and 'n' (e.g., "4n" becomes "4*n") using a loop
|
||||||
String withMultiplication = expression.replaceAll("(\\d)n", "$1*n");
|
StringBuilder sb = new StringBuilder(expression.length() + 4); // +4 for possible extra '*'
|
||||||
|
for (int i = 0; i < expression.length(); i++) {
|
||||||
|
char c = expression.charAt(i);
|
||||||
|
sb.append(c);
|
||||||
|
if (Character.isDigit(c)
|
||||||
|
&& i + 1 < expression.length()
|
||||||
|
&& expression.charAt(i + 1) == 'n') {
|
||||||
|
sb.append('*');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String withMultiplication = sb.toString();
|
||||||
withMultiplication = formatConsecutiveNsForNFunction(withMultiplication);
|
withMultiplication = formatConsecutiveNsForNFunction(withMultiplication);
|
||||||
// Now replace 'n' with its current value
|
// Now replace 'n' with its current value
|
||||||
return withMultiplication.replace("n", String.valueOf(nValue));
|
return withMultiplication.replace("n", String.valueOf(nValue));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String formatConsecutiveNsForNFunction(String expression) {
|
private String formatConsecutiveNsForNFunction(String expression) {
|
||||||
String text = expression;
|
String text = expression;
|
||||||
while (text.matches(".*n{2,}.*")) {
|
// Replace all consecutive 'nn' with 'n*n' until no more 'nn' is found
|
||||||
text = text.replaceAll("(?<!n)n{2}", "n*n");
|
while (text.contains("nn")) {
|
||||||
|
StringBuilder sb = new StringBuilder(text.length() + 2); // +2 for possible extra '*'
|
||||||
|
int i = 0;
|
||||||
|
while (i < text.length()) {
|
||||||
|
if (i < text.length() - 1 && text.charAt(i) == 'n' && text.charAt(i + 1) == 'n') {
|
||||||
|
sb.append("n*n");
|
||||||
|
i += 2;
|
||||||
|
} else {
|
||||||
|
sb.append(text.charAt(i));
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
text = sb.toString();
|
||||||
}
|
}
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<Integer> handlePart(String part, int totalPages, int offset) {
|
private List<Integer> handlePart(String part, int totalPages, int offset) {
|
||||||
List<Integer> partResult = new ArrayList<>();
|
List<Integer> partResult = new ArrayList<>();
|
||||||
|
|
||||||
// First check for n-syntax because it should not be processed as a range
|
// First check for n-syntax because it should not be processed as a range
|
||||||
@@ -361,7 +647,7 @@ public class GeneralUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (NumberFormatException e) {
|
} catch (NumberFormatException e) {
|
||||||
// Range is invalid, ignore this part
|
log.debug("Invalid range: {}", part);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// This is a single page number
|
// This is a single page number
|
||||||
@@ -370,14 +656,14 @@ public class GeneralUtils {
|
|||||||
if (pageNum >= 1 && pageNum <= totalPages) {
|
if (pageNum >= 1 && pageNum <= totalPages) {
|
||||||
partResult.add(pageNum - 1 + offset);
|
partResult.add(pageNum - 1 + offset);
|
||||||
}
|
}
|
||||||
} catch (NumberFormatException ignored) {
|
} catch (NumberFormatException e) {
|
||||||
// Ignore invalid numbers
|
log.debug("Invalid page number: {}", part);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return partResult;
|
return partResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean createDir(String path) {
|
public boolean createDir(String path) {
|
||||||
Path folder = Paths.get(path);
|
Path folder = Paths.get(path);
|
||||||
if (!Files.exists(folder)) {
|
if (!Files.exists(folder)) {
|
||||||
try {
|
try {
|
||||||
@@ -390,7 +676,7 @@ public class GeneralUtils {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isValidUUID(String uuid) {
|
public boolean isValidUUID(String uuid) {
|
||||||
if (uuid == null) {
|
if (uuid == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -406,7 +692,7 @@ public class GeneralUtils {
|
|||||||
* Internal Implementation Details *
|
* Internal Implementation Details *
|
||||||
*------------------------------------------------------------------------*/
|
*------------------------------------------------------------------------*/
|
||||||
|
|
||||||
public static void saveKeyToSettings(String key, Object newValue) throws IOException {
|
public void saveKeyToSettings(String key, Object newValue) throws IOException {
|
||||||
String[] keyArray = key.split("\\.");
|
String[] keyArray = key.split("\\.");
|
||||||
Path settingsPath = Paths.get(InstallationPathConfig.getSettingsPath());
|
Path settingsPath = Paths.get(InstallationPathConfig.getSettingsPath());
|
||||||
YamlHelper settingsYaml = new YamlHelper(settingsPath);
|
YamlHelper settingsYaml = new YamlHelper(settingsPath);
|
||||||
@@ -414,48 +700,67 @@ public class GeneralUtils {
|
|||||||
settingsYaml.saveOverride(settingsPath);
|
settingsYaml.saveOverride(settingsPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String generateMachineFingerprint() {
|
/*
|
||||||
|
* Machine fingerprint generation with better error logging and fallbacks.
|
||||||
|
*
|
||||||
|
* @return unique machine fingerprint or "GenericID" if generation fails
|
||||||
|
*/
|
||||||
|
public String generateMachineFingerprint() {
|
||||||
try {
|
try {
|
||||||
// Get the MAC address
|
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
|
|
||||||
|
// Try to get MAC address from primary network interface
|
||||||
InetAddress ip = InetAddress.getLocalHost();
|
InetAddress ip = InetAddress.getLocalHost();
|
||||||
NetworkInterface network = NetworkInterface.getByInetAddress(ip);
|
NetworkInterface network = NetworkInterface.getByInetAddress(ip);
|
||||||
|
|
||||||
if (network == null) {
|
if (network == null || network.getHardwareAddress() == null) {
|
||||||
|
// Fallback: iterate through all network interfaces
|
||||||
Enumeration<NetworkInterface> networks = NetworkInterface.getNetworkInterfaces();
|
Enumeration<NetworkInterface> networks = NetworkInterface.getNetworkInterfaces();
|
||||||
while (networks.hasMoreElements()) {
|
while (networks.hasMoreElements()) {
|
||||||
NetworkInterface net = networks.nextElement();
|
NetworkInterface net = networks.nextElement();
|
||||||
|
if (net.isUp() && !net.isLoopback() && !net.isVirtual()) {
|
||||||
byte[] mac = net.getHardwareAddress();
|
byte[] mac = net.getHardwareAddress();
|
||||||
if (mac != null) {
|
if (mac != null && mac.length > 0) {
|
||||||
for (int i = 0; i < mac.length; i++) {
|
for (byte b : mac) {
|
||||||
sb.append(String.format("%02X", mac[i]));
|
sb.append(String.format("%02X", b));
|
||||||
|
}
|
||||||
|
break; // Use the first valid network interface
|
||||||
}
|
}
|
||||||
break; // Use the first network interface with a MAC address
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
byte[] mac = network.getHardwareAddress();
|
byte[] mac = network.getHardwareAddress();
|
||||||
if (mac != null) {
|
if (mac != null) {
|
||||||
for (int i = 0; i < mac.length; i++) {
|
for (byte b : mac) {
|
||||||
sb.append(String.format("%02X", mac[i]));
|
sb.append(String.format("%02X", b));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hash the MAC address for privacy and consistency
|
// If no MAC address found, use hostname as fallback
|
||||||
|
if (sb.length() == 0) {
|
||||||
|
String hostname = InetAddress.getLocalHost().getHostName();
|
||||||
|
sb.append(hostname != null ? hostname : "unknown-host");
|
||||||
|
log.warn("No MAC address found, using hostname for fingerprint generation");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash the collected data for privacy and consistency
|
||||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||||
byte[] hash = md.digest(sb.toString().getBytes(StandardCharsets.UTF_8));
|
byte[] hash = md.digest(sb.toString().getBytes(StandardCharsets.UTF_8));
|
||||||
StringBuilder fingerprint = new StringBuilder();
|
StringBuilder fingerprint = new StringBuilder();
|
||||||
for (byte b : hash) {
|
for (byte b : hash) {
|
||||||
fingerprint.append(String.format("%02x", b));
|
fingerprint.append(String.format("%02x", b));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.debug("Successfully generated machine fingerprint");
|
||||||
return fingerprint.toString();
|
return fingerprint.toString();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
log.warn("Failed to generate machine fingerprint: {}", e.getMessage());
|
||||||
return "GenericID";
|
return "GenericID";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/*
|
||||||
* Extracts the default pipeline configurations from the classpath to the installation path.
|
* Extracts the default pipeline configurations from the classpath to the installation path.
|
||||||
* Creates directories if needed and copies default JSON files.
|
* Creates directories if needed and copies default JSON files.
|
||||||
*
|
*
|
||||||
@@ -464,7 +769,7 @@ public class GeneralUtils {
|
|||||||
*
|
*
|
||||||
* @throws IOException if an I/O error occurs during file operations
|
* @throws IOException if an I/O error occurs during file operations
|
||||||
*/
|
*/
|
||||||
public static void extractPipeline() throws IOException {
|
public void extractPipeline() throws IOException {
|
||||||
Path pipelineDir =
|
Path pipelineDir =
|
||||||
Paths.get(InstallationPathConfig.getPipelinePath(), DEFAULT_WEBUI_CONFIGS_DIR);
|
Paths.get(InstallationPathConfig.getPipelinePath(), DEFAULT_WEBUI_CONFIGS_DIR);
|
||||||
Files.createDirectories(pipelineDir);
|
Files.createDirectories(pipelineDir);
|
||||||
@@ -486,7 +791,7 @@ public class GeneralUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/*
|
||||||
* Extracts the specified Python script from the classpath to the installation path. Validates
|
* Extracts the specified Python script from the classpath to the installation path. Validates
|
||||||
* name and copies file atomically when possible, overwriting existing.
|
* name and copies file atomically when possible, overwriting existing.
|
||||||
*
|
*
|
||||||
@@ -497,7 +802,7 @@ public class GeneralUtils {
|
|||||||
* @throws IllegalArgumentException if the script name is invalid or not allowed
|
* @throws IllegalArgumentException if the script name is invalid or not allowed
|
||||||
* @throws IOException if an I/O error occurs
|
* @throws IOException if an I/O error occurs
|
||||||
*/
|
*/
|
||||||
public static Path extractScript(String scriptName) throws IOException {
|
public Path extractScript(String scriptName) throws IOException {
|
||||||
// Validate input
|
// Validate input
|
||||||
if (scriptName == null || scriptName.trim().isEmpty()) {
|
if (scriptName == null || scriptName.trim().isEmpty()) {
|
||||||
throw new IllegalArgumentException("scriptName must not be null or empty");
|
throw new IllegalArgumentException("scriptName must not be null or empty");
|
||||||
@@ -530,15 +835,14 @@ public class GeneralUtils {
|
|||||||
return target;
|
return target;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/*
|
||||||
* Copies a resource from the classpath to a specified target file.
|
* Copies a resource from the classpath to a specified target file.
|
||||||
*
|
*
|
||||||
* @param resource the ClassPathResource to copy
|
* @param resource the ClassPathResource to copy
|
||||||
* @param target the target Path where the resource will be copied
|
* @param target the target Path where the resource will be copied
|
||||||
* @throws IOException if an I/O error occurs during the copy operation
|
* @throws IOException if an I/O error occurs during the copy operation
|
||||||
*/
|
*/
|
||||||
private static void copyResourceToFile(ClassPathResource resource, Path target)
|
private void copyResourceToFile(ClassPathResource resource, Path target) throws IOException {
|
||||||
throws IOException {
|
|
||||||
Path dir = target.getParent();
|
Path dir = target.getParent();
|
||||||
Path tmp = Files.createTempFile(dir, target.getFileName().toString(), ".tmp");
|
Path tmp = Files.createTempFile(dir, target.getFileName().toString(), ".tmp");
|
||||||
try (InputStream in = resource.getInputStream()) {
|
try (InputStream in = resource.getInputStream()) {
|
||||||
@@ -573,7 +877,7 @@ public class GeneralUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isVersionHigher(String currentVersion, String compareVersion) {
|
public boolean isVersionHigher(String currentVersion, String compareVersion) {
|
||||||
if (currentVersion == null || compareVersion == null) {
|
if (currentVersion == null || compareVersion == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -601,4 +905,67 @@ public class GeneralUtils {
|
|||||||
// If all components so far are equal, the longer version is considered higher
|
// If all components so far are equal, the longer version is considered higher
|
||||||
return current.length > compare.length;
|
return current.length > compare.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optimizes a PDF using Ghostscript with ebook settings for better e-reader compatibility. Uses
|
||||||
|
* -dPDFSETTINGS=/ebook -dFastWebView=true settings to create an optimized PDF.
|
||||||
|
*
|
||||||
|
* @param inputPdfBytes Original PDF as byte array
|
||||||
|
* @return Optimized PDF as byte array
|
||||||
|
* @throws IOException if Ghostscript optimization fails
|
||||||
|
*/
|
||||||
|
public byte[] optimizePdfWithGhostscript(byte[] inputPdfBytes) throws IOException {
|
||||||
|
Path tempInput = null;
|
||||||
|
Path tempOutput = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
tempInput = Files.createTempFile("gs_input_", ".pdf");
|
||||||
|
tempOutput = Files.createTempFile("gs_output_", ".pdf");
|
||||||
|
|
||||||
|
Files.write(tempInput, inputPdfBytes);
|
||||||
|
|
||||||
|
List<String> command = new ArrayList<>();
|
||||||
|
command.add("gs");
|
||||||
|
command.add("-sDEVICE=pdfwrite");
|
||||||
|
command.add("-dPDFSETTINGS=/ebook");
|
||||||
|
command.add("-dFastWebView=true");
|
||||||
|
command.add("-dNOPAUSE");
|
||||||
|
command.add("-dQUIET");
|
||||||
|
command.add("-dBATCH");
|
||||||
|
command.add("-sOutputFile=" + tempOutput.toString());
|
||||||
|
command.add(tempInput.toString());
|
||||||
|
|
||||||
|
ProcessExecutor.ProcessExecutorResult result =
|
||||||
|
ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT)
|
||||||
|
.runCommandWithOutputHandling(command);
|
||||||
|
|
||||||
|
if (result.getRc() != 0) {
|
||||||
|
log.warn(
|
||||||
|
"Ghostscript ebook optimization failed with return code: {}",
|
||||||
|
result.getRc());
|
||||||
|
throw ExceptionUtils.createGhostscriptCompressionException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Files.readAllBytes(tempOutput);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Ghostscript ebook optimization failed", e);
|
||||||
|
throw ExceptionUtils.createGhostscriptCompressionException(e);
|
||||||
|
} finally {
|
||||||
|
if (tempInput != null) {
|
||||||
|
try {
|
||||||
|
Files.deleteIfExists(tempInput);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("Failed to delete temp input file: {}", tempInput, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (tempOutput != null) {
|
||||||
|
try {
|
||||||
|
Files.deleteIfExists(tempOutput);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("Failed to delete temp output file: {}", tempOutput, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,19 +85,16 @@ public class ImageProcessingUtils {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
int orientationTag = directory.getInt(ExifSubIFDDirectory.TAG_ORIENTATION);
|
int orientationTag = directory.getInt(ExifSubIFDDirectory.TAG_ORIENTATION);
|
||||||
switch (orientationTag) {
|
return switch (orientationTag) {
|
||||||
case 1:
|
case 1 -> 0;
|
||||||
return 0;
|
case 6 -> 90;
|
||||||
case 6:
|
case 3 -> 180;
|
||||||
return 90;
|
case 8 -> 270;
|
||||||
case 3:
|
default -> {
|
||||||
return 180;
|
|
||||||
case 8:
|
|
||||||
return 270;
|
|
||||||
default:
|
|
||||||
log.warn("Unknown orientation tag: {}", orientationTag);
|
log.warn("Unknown orientation tag: {}", orientationTag);
|
||||||
return 0;
|
yield 0;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
} catch (ImageProcessingException | MetadataException e) {
|
} catch (ImageProcessingException | MetadataException e) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package stirling.software.common.util;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.multipdf.PDFMergerUtility;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class PDFService {
|
||||||
|
|
||||||
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Merge multiple PDF documents into a single PDF document
|
||||||
|
*
|
||||||
|
* @param documents List of PDDocument to be merged
|
||||||
|
* @return Merged PDDocument
|
||||||
|
* @throws IOException If an error occurs during merging
|
||||||
|
*/
|
||||||
|
public PDDocument mergeDocuments(List<PDDocument> documents) throws IOException {
|
||||||
|
PDDocument merged = pdfDocumentFactory.createNewDocument();
|
||||||
|
PDFMergerUtility merger = new PDFMergerUtility();
|
||||||
|
for (PDDocument doc : documents) {
|
||||||
|
merger.appendDocument(merged, doc);
|
||||||
|
}
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,18 +25,22 @@ import com.vladsch.flexmark.util.data.MutableDataSet;
|
|||||||
|
|
||||||
import io.github.pixee.security.Filenames;
|
import io.github.pixee.security.Filenames;
|
||||||
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@NoArgsConstructor
|
|
||||||
public class PDFToFile {
|
public class PDFToFile {
|
||||||
|
|
||||||
|
private final TempFileManager tempFileManager;
|
||||||
|
|
||||||
|
public PDFToFile(TempFileManager tempFileManager) {
|
||||||
|
this.tempFileManager = tempFileManager;
|
||||||
|
}
|
||||||
|
|
||||||
public ResponseEntity<byte[]> processPdfToMarkdown(MultipartFile inputFile)
|
public ResponseEntity<byte[]> processPdfToMarkdown(MultipartFile inputFile)
|
||||||
throws IOException, InterruptedException {
|
throws IOException, InterruptedException {
|
||||||
if (!"application/pdf".equals(inputFile.getContentType())) {
|
if (!MediaType.APPLICATION_PDF_VALUE.equals(inputFile.getContentType())) {
|
||||||
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
|
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,15 +75,12 @@ public class PDFToFile {
|
|||||||
pdfBaseName = originalPdfFileName.substring(0, originalPdfFileName.lastIndexOf('.'));
|
pdfBaseName = originalPdfFileName.substring(0, originalPdfFileName.lastIndexOf('.'));
|
||||||
}
|
}
|
||||||
|
|
||||||
Path tempInputFile = null;
|
|
||||||
Path tempOutputDir = null;
|
|
||||||
byte[] fileBytes;
|
byte[] fileBytes;
|
||||||
String fileName = "temp.file";
|
String fileName;
|
||||||
|
|
||||||
try {
|
try (TempFile tempInputFile = new TempFile(tempFileManager, ".pdf");
|
||||||
tempInputFile = Files.createTempFile("input_", ".pdf");
|
TempDirectory tempOutputDir = new TempDirectory(tempFileManager)) {
|
||||||
inputFile.transferTo(tempInputFile);
|
inputFile.transferTo(tempInputFile.getFile());
|
||||||
tempOutputDir = Files.createTempDirectory("output_");
|
|
||||||
|
|
||||||
List<String> command =
|
List<String> command =
|
||||||
new ArrayList<>(
|
new ArrayList<>(
|
||||||
@@ -88,14 +89,16 @@ public class PDFToFile {
|
|||||||
"-s",
|
"-s",
|
||||||
"-noframes",
|
"-noframes",
|
||||||
"-c",
|
"-c",
|
||||||
tempInputFile.toString(),
|
tempInputFile.getAbsolutePath(),
|
||||||
pdfBaseName));
|
pdfBaseName));
|
||||||
|
|
||||||
ProcessExecutorResult returnCode =
|
ProcessExecutorResult returnCode =
|
||||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.PDFTOHTML)
|
ProcessExecutor.getInstance(ProcessExecutor.Processes.PDFTOHTML)
|
||||||
.runCommandWithOutputHandling(command, tempOutputDir.toFile());
|
.runCommandWithOutputHandling(
|
||||||
|
command, tempOutputDir.getPath().toFile());
|
||||||
// Process HTML files to Markdown
|
// Process HTML files to Markdown
|
||||||
File[] outputFiles = Objects.requireNonNull(tempOutputDir.toFile().listFiles());
|
File[] outputFiles =
|
||||||
|
Objects.requireNonNull(tempOutputDir.getPath().toFile().listFiles());
|
||||||
List<File> markdownFiles = new ArrayList<>();
|
List<File> markdownFiles = new ArrayList<>();
|
||||||
|
|
||||||
// Convert HTML files to Markdown
|
// Convert HTML files to Markdown
|
||||||
@@ -105,7 +108,7 @@ public class PDFToFile {
|
|||||||
String markdown = htmlToMarkdownConverter.convert(html);
|
String markdown = htmlToMarkdownConverter.convert(html);
|
||||||
|
|
||||||
String mdFileName = outputFile.getName().replace(".html", ".md");
|
String mdFileName = outputFile.getName().replace(".html", ".md");
|
||||||
File mdFile = new File(tempOutputDir.toFile(), mdFileName);
|
File mdFile = new File(tempOutputDir.getPath().toFile(), mdFileName);
|
||||||
Files.writeString(mdFile.toPath(), markdown);
|
Files.writeString(mdFile.toPath(), markdown);
|
||||||
markdownFiles.add(mdFile);
|
markdownFiles.add(mdFile);
|
||||||
}
|
}
|
||||||
@@ -142,10 +145,6 @@ public class PDFToFile {
|
|||||||
|
|
||||||
fileBytes = byteArrayOutputStream.toByteArray();
|
fileBytes = byteArrayOutputStream.toByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
} finally {
|
|
||||||
if (tempInputFile != null) Files.deleteIfExists(tempInputFile);
|
|
||||||
if (tempOutputDir != null) FileUtils.deleteDirectory(tempOutputDir.toFile());
|
|
||||||
}
|
}
|
||||||
return WebResponseUtils.bytesToWebResponse(
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
fileBytes, fileName, MediaType.APPLICATION_OCTET_STREAM);
|
fileBytes, fileName, MediaType.APPLICATION_OCTET_STREAM);
|
||||||
@@ -153,7 +152,7 @@ public class PDFToFile {
|
|||||||
|
|
||||||
public ResponseEntity<byte[]> processPdfToHtml(MultipartFile inputFile)
|
public ResponseEntity<byte[]> processPdfToHtml(MultipartFile inputFile)
|
||||||
throws IOException, InterruptedException {
|
throws IOException, InterruptedException {
|
||||||
if (!"application/pdf".equals(inputFile.getContentType())) {
|
if (!MediaType.APPLICATION_PDF_VALUE.equals(inputFile.getContentType())) {
|
||||||
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
|
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,19 +163,18 @@ public class PDFToFile {
|
|||||||
pdfBaseName = originalPdfFileName.substring(0, originalPdfFileName.lastIndexOf('.'));
|
pdfBaseName = originalPdfFileName.substring(0, originalPdfFileName.lastIndexOf('.'));
|
||||||
}
|
}
|
||||||
|
|
||||||
Path tempInputFile = null;
|
|
||||||
Path tempOutputDir = null;
|
|
||||||
byte[] fileBytes;
|
byte[] fileBytes;
|
||||||
String fileName = "temp.file";
|
String fileName;
|
||||||
|
|
||||||
|
try (TempFile inputFileTemp = new TempFile(tempFileManager, ".pdf");
|
||||||
|
TempDirectory outputDirTemp = new TempDirectory(tempFileManager)) {
|
||||||
|
|
||||||
|
Path tempInputFile = inputFileTemp.getPath();
|
||||||
|
Path tempOutputDir = outputDirTemp.getPath();
|
||||||
|
|
||||||
try {
|
|
||||||
// Save the uploaded file to a temporary location
|
// Save the uploaded file to a temporary location
|
||||||
tempInputFile = Files.createTempFile("input_", ".pdf");
|
|
||||||
inputFile.transferTo(tempInputFile);
|
inputFile.transferTo(tempInputFile);
|
||||||
|
|
||||||
// Prepare the output directory
|
|
||||||
tempOutputDir = Files.createTempDirectory("output_");
|
|
||||||
|
|
||||||
// Run the pdftohtml command with complex output
|
// Run the pdftohtml command with complex output
|
||||||
List<String> command =
|
List<String> command =
|
||||||
new ArrayList<>(
|
new ArrayList<>(
|
||||||
@@ -208,11 +206,6 @@ public class PDFToFile {
|
|||||||
log.error("Exception writing zip", e);
|
log.error("Exception writing zip", e);
|
||||||
}
|
}
|
||||||
fileBytes = byteArrayOutputStream.toByteArray();
|
fileBytes = byteArrayOutputStream.toByteArray();
|
||||||
|
|
||||||
} finally {
|
|
||||||
// Clean up the temporary files
|
|
||||||
if (tempInputFile != null) Files.deleteIfExists(tempInputFile);
|
|
||||||
if (tempOutputDir != null) FileUtils.deleteDirectory(tempOutputDir.toFile());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return WebResponseUtils.bytesToWebResponse(
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
@@ -223,14 +216,14 @@ public class PDFToFile {
|
|||||||
MultipartFile inputFile, String outputFormat, String libreOfficeFilter)
|
MultipartFile inputFile, String outputFormat, String libreOfficeFilter)
|
||||||
throws IOException, InterruptedException {
|
throws IOException, InterruptedException {
|
||||||
|
|
||||||
if (!"application/pdf".equals(inputFile.getContentType())) {
|
if (!MediaType.APPLICATION_PDF_VALUE.equals(inputFile.getContentType())) {
|
||||||
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
|
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the original PDF file name without the extension
|
// Get the original PDF file name without the extension
|
||||||
String originalPdfFileName = Filenames.toSimpleFileName(inputFile.getOriginalFilename());
|
String originalPdfFileName = Filenames.toSimpleFileName(inputFile.getOriginalFilename());
|
||||||
|
|
||||||
if (originalPdfFileName == null || "".equals(originalPdfFileName.trim())) {
|
if (originalPdfFileName == null || originalPdfFileName.trim().isEmpty()) {
|
||||||
originalPdfFileName = "output.pdf";
|
originalPdfFileName = "output.pdf";
|
||||||
}
|
}
|
||||||
// Assume file is pdf if no extension
|
// Assume file is pdf if no extension
|
||||||
@@ -245,19 +238,18 @@ public class PDFToFile {
|
|||||||
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
|
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
Path tempInputFile = null;
|
|
||||||
Path tempOutputDir = null;
|
|
||||||
byte[] fileBytes;
|
byte[] fileBytes;
|
||||||
String fileName = "temp.file";
|
String fileName;
|
||||||
|
|
||||||
|
try (TempFile inputFileTemp = new TempFile(tempFileManager, ".pdf");
|
||||||
|
TempDirectory outputDirTemp = new TempDirectory(tempFileManager)) {
|
||||||
|
|
||||||
|
Path tempInputFile = inputFileTemp.getPath();
|
||||||
|
Path tempOutputDir = outputDirTemp.getPath();
|
||||||
|
|
||||||
try {
|
|
||||||
// Save the uploaded file to a temporary location
|
// Save the uploaded file to a temporary location
|
||||||
tempInputFile = Files.createTempFile("input_", ".pdf");
|
|
||||||
inputFile.transferTo(tempInputFile);
|
inputFile.transferTo(tempInputFile);
|
||||||
|
|
||||||
// Prepare the output directory
|
|
||||||
tempOutputDir = Files.createTempDirectory("output_");
|
|
||||||
|
|
||||||
// Run the LibreOffice command
|
// Run the LibreOffice command
|
||||||
List<String> command =
|
List<String> command =
|
||||||
new ArrayList<>(
|
new ArrayList<>(
|
||||||
@@ -308,11 +300,6 @@ public class PDFToFile {
|
|||||||
|
|
||||||
fileBytes = byteArrayOutputStream.toByteArray();
|
fileBytes = byteArrayOutputStream.toByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
} finally {
|
|
||||||
// Clean up the temporary files
|
|
||||||
Files.deleteIfExists(tempInputFile);
|
|
||||||
if (tempOutputDir != null) FileUtils.deleteDirectory(tempOutputDir.toFile());
|
|
||||||
}
|
}
|
||||||
return WebResponseUtils.bytesToWebResponse(
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
fileBytes, fileName, MediaType.APPLICATION_OCTET_STREAM);
|
fileBytes, fileName, MediaType.APPLICATION_OCTET_STREAM);
|
||||||
|
|||||||
+144
-132
@@ -8,7 +8,9 @@ import java.io.File;
|
|||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.text.SimpleDateFormat;
|
import java.time.ZoneId;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
@@ -18,7 +20,6 @@ import java.util.List;
|
|||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.TimeZone;
|
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
@@ -37,6 +38,7 @@ import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream;
|
|||||||
import org.apache.pdfbox.text.PDFTextStripper;
|
import org.apache.pdfbox.text.PDFTextStripper;
|
||||||
import org.apache.pdfbox.text.TextPosition;
|
import org.apache.pdfbox.text.TextPosition;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
@@ -118,7 +120,7 @@ public class PdfAttachmentHandler {
|
|||||||
public String getContentType() {
|
public String getContentType() {
|
||||||
return attachment.getContentType() != null
|
return attachment.getContentType() != null
|
||||||
? attachment.getContentType()
|
? attachment.getContentType()
|
||||||
: "application/octet-stream";
|
: MediaType.APPLICATION_OCTET_STREAM_VALUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -256,10 +258,7 @@ public class PdfAttachmentHandler {
|
|||||||
|
|
||||||
if (contentIdMap.isEmpty()) return htmlContent;
|
if (contentIdMap.isEmpty()) return htmlContent;
|
||||||
|
|
||||||
Pattern cidPattern =
|
Pattern cidPattern = RegexPatternUtils.getInstance().getInlineCidImagePattern();
|
||||||
Pattern.compile(
|
|
||||||
"(?i)<img[^>]*\\ssrc\\s*=\\s*['\"]cid:([^'\"]+)['\"][^>]*>",
|
|
||||||
Pattern.CASE_INSENSITIVE);
|
|
||||||
Matcher matcher = cidPattern.matcher(htmlContent);
|
Matcher matcher = cidPattern.matcher(htmlContent);
|
||||||
|
|
||||||
StringBuilder result = new StringBuilder();
|
StringBuilder result = new StringBuilder();
|
||||||
@@ -289,11 +288,15 @@ public class PdfAttachmentHandler {
|
|||||||
|
|
||||||
public static String formatEmailDate(Date date) {
|
public static String formatEmailDate(Date date) {
|
||||||
if (date == null) return "";
|
if (date == null) return "";
|
||||||
|
return formatEmailDate(ZonedDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault()));
|
||||||
|
}
|
||||||
|
|
||||||
SimpleDateFormat formatter =
|
public static String formatEmailDate(ZonedDateTime dateTime) {
|
||||||
new SimpleDateFormat("EEE, MMM d, yyyy 'at' h:mm a z", Locale.ENGLISH);
|
if (dateTime == null) return "";
|
||||||
formatter.setTimeZone(TimeZone.getTimeZone("UTC"));
|
|
||||||
return formatter.format(date);
|
DateTimeFormatter formatter =
|
||||||
|
DateTimeFormatter.ofPattern("EEE, MMM d, yyyy 'at' h:mm a z", Locale.ENGLISH);
|
||||||
|
return dateTime.withZoneSameInstant(ZoneId.of("UTC")).format(formatter);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@@ -313,121 +316,20 @@ public class PdfAttachmentHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class AttachmentMarkerPositionFinder extends PDFTextStripper {
|
private static String normalizeFilename(String filename) {
|
||||||
@Getter private final List<MarkerPosition> positions = new ArrayList<>();
|
if (filename == null) return "";
|
||||||
private int currentPageIndex;
|
String normalized = filename.toLowerCase().trim();
|
||||||
protected boolean sortByPosition;
|
normalized =
|
||||||
private boolean isInAttachmentSection;
|
RegexPatternUtils.getInstance()
|
||||||
private boolean attachmentSectionFound;
|
.getWhitespacePattern()
|
||||||
private final StringBuilder currentText = new StringBuilder();
|
.matcher(normalized)
|
||||||
|
.replaceAll(" ");
|
||||||
private static final Pattern ATTACHMENT_SECTION_PATTERN =
|
normalized =
|
||||||
Pattern.compile("attachments\\s*\\(\\d+\\)", Pattern.CASE_INSENSITIVE);
|
RegexPatternUtils.getInstance()
|
||||||
|
.getPattern("[^a-zA-Z0-9._-]")
|
||||||
private static final Pattern FILENAME_PATTERN =
|
.matcher(normalized)
|
||||||
Pattern.compile("@\\s*([^\\s\\(]+(?:\\.[a-zA-Z0-9]+)?)");
|
.replaceAll("");
|
||||||
|
return normalized;
|
||||||
public AttachmentMarkerPositionFinder() {
|
|
||||||
super();
|
|
||||||
this.currentPageIndex = 0;
|
|
||||||
this.sortByPosition = false; // Disable sorting to preserve document order
|
|
||||||
this.isInAttachmentSection = false;
|
|
||||||
this.attachmentSectionFound = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getText(PDDocument document) throws IOException {
|
|
||||||
super.getText(document);
|
|
||||||
|
|
||||||
if (sortByPosition) {
|
|
||||||
positions.sort(
|
|
||||||
(a, b) -> {
|
|
||||||
int pageCompare = Integer.compare(a.getPageIndex(), b.getPageIndex());
|
|
||||||
if (pageCompare != 0) return pageCompare;
|
|
||||||
return Float.compare(
|
|
||||||
b.getY(), a.getY()); // Descending Y per PDF coordinate system
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return ""; // Return empty string as we only need positions
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void startPage(PDPage page) throws IOException {
|
|
||||||
super.startPage(page);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void endPage(PDPage page) throws IOException {
|
|
||||||
currentPageIndex++;
|
|
||||||
super.endPage(page);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void writeString(String string, List<TextPosition> textPositions)
|
|
||||||
throws IOException {
|
|
||||||
String lowerString = string.toLowerCase();
|
|
||||||
|
|
||||||
if (ATTACHMENT_SECTION_PATTERN.matcher(lowerString).find()) {
|
|
||||||
isInAttachmentSection = true;
|
|
||||||
attachmentSectionFound = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isInAttachmentSection
|
|
||||||
&& (lowerString.contains("</body>")
|
|
||||||
|| lowerString.contains("</html>")
|
|
||||||
|| (attachmentSectionFound
|
|
||||||
&& lowerString.trim().isEmpty()
|
|
||||||
&& string.length() > 50))) {
|
|
||||||
isInAttachmentSection = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isInAttachmentSection) {
|
|
||||||
currentText.append(string);
|
|
||||||
|
|
||||||
for (int i = 0; (i = string.indexOf(ATTACHMENT_MARKER, i)) != -1; i++) {
|
|
||||||
if (i < textPositions.size()) {
|
|
||||||
TextPosition textPosition = textPositions.get(i);
|
|
||||||
|
|
||||||
String filename = extractFilenameAfterMarker(string, i);
|
|
||||||
|
|
||||||
MarkerPosition position =
|
|
||||||
new MarkerPosition(
|
|
||||||
currentPageIndex,
|
|
||||||
textPosition.getXDirAdj(),
|
|
||||||
textPosition.getYDirAdj(),
|
|
||||||
ATTACHMENT_MARKER,
|
|
||||||
filename);
|
|
||||||
positions.add(position);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
super.writeString(string, textPositions);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setSortByPosition(boolean sortByPosition) {
|
|
||||||
this.sortByPosition = sortByPosition;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String extractFilenameAfterMarker(String text, int markerIndex) {
|
|
||||||
String afterMarker = text.substring(markerIndex + 1);
|
|
||||||
|
|
||||||
Matcher matcher = FILENAME_PATTERN.matcher("@" + afterMarker);
|
|
||||||
if (matcher.find()) {
|
|
||||||
return matcher.group(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
String[] parts = afterMarker.split("[\\s\\(\\)]+");
|
|
||||||
for (String part : parts) {
|
|
||||||
part = part.trim();
|
|
||||||
if (part.length() > 3 && part.contains(".")) {
|
|
||||||
return part;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Map<Integer, String> addAttachmentsToDocumentWithMapping(
|
private static Map<Integer, String> addAttachmentsToDocumentWithMapping(
|
||||||
@@ -607,12 +509,122 @@ public class PdfAttachmentHandler {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String normalizeFilename(String filename) {
|
public static class AttachmentMarkerPositionFinder extends PDFTextStripper {
|
||||||
if (filename == null) return "";
|
private static final Pattern ATTACHMENT_SECTION_PATTERN =
|
||||||
return filename.toLowerCase()
|
RegexPatternUtils.getInstance().getAttachmentSectionPattern();
|
||||||
.trim()
|
private static final Pattern FILENAME_PATTERN =
|
||||||
.replaceAll("\\s+", " ")
|
RegexPatternUtils.getInstance().getAttachmentFilenamePattern();
|
||||||
.replaceAll("[^a-zA-Z0-9._-]", "");
|
@Getter private final List<MarkerPosition> positions = new ArrayList<>();
|
||||||
|
private final StringBuilder currentText = new StringBuilder();
|
||||||
|
protected boolean sortByPosition;
|
||||||
|
private int currentPageIndex;
|
||||||
|
private boolean isInAttachmentSection;
|
||||||
|
private boolean attachmentSectionFound;
|
||||||
|
|
||||||
|
public AttachmentMarkerPositionFinder() {
|
||||||
|
super();
|
||||||
|
this.currentPageIndex = 0;
|
||||||
|
this.sortByPosition = false; // Disable sorting to preserve document order
|
||||||
|
this.isInAttachmentSection = false;
|
||||||
|
this.attachmentSectionFound = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getText(PDDocument document) throws IOException {
|
||||||
|
super.getText(document);
|
||||||
|
|
||||||
|
if (sortByPosition) {
|
||||||
|
positions.sort(
|
||||||
|
(a, b) -> {
|
||||||
|
int pageCompare = Integer.compare(a.getPageIndex(), b.getPageIndex());
|
||||||
|
if (pageCompare != 0) return pageCompare;
|
||||||
|
return Float.compare(
|
||||||
|
b.getY(), a.getY()); // Descending Y per PDF coordinate system
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""; // Return empty string as we only need positions
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void startPage(PDPage page) throws IOException {
|
||||||
|
super.startPage(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void endPage(PDPage page) throws IOException {
|
||||||
|
currentPageIndex++;
|
||||||
|
super.endPage(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void writeString(String string, List<TextPosition> textPositions)
|
||||||
|
throws IOException {
|
||||||
|
String lowerString = string.toLowerCase();
|
||||||
|
|
||||||
|
if (ATTACHMENT_SECTION_PATTERN.matcher(lowerString).find()) {
|
||||||
|
isInAttachmentSection = true;
|
||||||
|
attachmentSectionFound = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isInAttachmentSection
|
||||||
|
&& (lowerString.contains("</body>")
|
||||||
|
|| lowerString.contains("</html>")
|
||||||
|
|| (attachmentSectionFound
|
||||||
|
&& lowerString.trim().isEmpty()
|
||||||
|
&& string.length() > 50))) {
|
||||||
|
isInAttachmentSection = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isInAttachmentSection) {
|
||||||
|
currentText.append(string);
|
||||||
|
|
||||||
|
for (int i = 0; (i = string.indexOf(ATTACHMENT_MARKER, i)) != -1; i++) {
|
||||||
|
if (i < textPositions.size()) {
|
||||||
|
TextPosition textPosition = textPositions.get(i);
|
||||||
|
|
||||||
|
String filename = extractFilenameAfterMarker(string, i);
|
||||||
|
|
||||||
|
MarkerPosition position =
|
||||||
|
new MarkerPosition(
|
||||||
|
currentPageIndex,
|
||||||
|
textPosition.getXDirAdj(),
|
||||||
|
textPosition.getYDirAdj(),
|
||||||
|
ATTACHMENT_MARKER,
|
||||||
|
filename);
|
||||||
|
positions.add(position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
super.writeString(string, textPositions);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setSortByPosition(boolean sortByPosition) {
|
||||||
|
this.sortByPosition = sortByPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractFilenameAfterMarker(String text, int markerIndex) {
|
||||||
|
String afterMarker = text.substring(markerIndex + 1);
|
||||||
|
|
||||||
|
Matcher matcher = FILENAME_PATTERN.matcher("@" + afterMarker);
|
||||||
|
if (matcher.find()) {
|
||||||
|
return matcher.group(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] parts =
|
||||||
|
RegexPatternUtils.getInstance()
|
||||||
|
.getWhitespaceParenthesesSplitPattern()
|
||||||
|
.split(afterMarker);
|
||||||
|
for (String part : parts) {
|
||||||
|
part = part.trim();
|
||||||
|
if (part.length() > 3 && part.contains(".")) {
|
||||||
|
return part;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void addAttachmentAnnotationToPageWithMapping(
|
private static void addAttachmentAnnotationToPageWithMapping(
|
||||||
|
|||||||
@@ -0,0 +1,173 @@
|
|||||||
|
package stirling.software.common.util;
|
||||||
|
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
|
||||||
|
import org.apache.commons.io.FilenameUtils;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.rendering.ImageType;
|
||||||
|
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
|
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class PdfToCbrUtils {
|
||||||
|
|
||||||
|
public static byte[] convertPdfToCbr(
|
||||||
|
MultipartFile pdfFile, int dpi, CustomPDFDocumentFactory pdfDocumentFactory)
|
||||||
|
throws IOException {
|
||||||
|
|
||||||
|
validatePdfFile(pdfFile);
|
||||||
|
|
||||||
|
try (PDDocument document = pdfDocumentFactory.load(pdfFile)) {
|
||||||
|
if (document.getNumberOfPages() == 0) {
|
||||||
|
throw new IllegalArgumentException("PDF file contains no pages");
|
||||||
|
}
|
||||||
|
|
||||||
|
return createCbrFromPdf(document, dpi);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validatePdfFile(MultipartFile file) {
|
||||||
|
if (file == null || file.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("File cannot be null or empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
String filename = file.getOriginalFilename();
|
||||||
|
if (filename == null) {
|
||||||
|
throw new IllegalArgumentException("File must have a name");
|
||||||
|
}
|
||||||
|
|
||||||
|
String extension = FilenameUtils.getExtension(filename).toLowerCase();
|
||||||
|
if (!"pdf".equals(extension)) {
|
||||||
|
throw new IllegalArgumentException("File must be a PDF");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] createCbrFromPdf(PDDocument document, int dpi) throws IOException {
|
||||||
|
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
||||||
|
|
||||||
|
Path tempDir = Files.createTempDirectory("stirling-pdf-cbr-");
|
||||||
|
List<Path> generatedImages = new ArrayList<>();
|
||||||
|
try {
|
||||||
|
int totalPages = document.getNumberOfPages();
|
||||||
|
|
||||||
|
for (int pageIndex = 0; pageIndex < totalPages; pageIndex++) {
|
||||||
|
try {
|
||||||
|
BufferedImage image =
|
||||||
|
pdfRenderer.renderImageWithDPI(pageIndex, dpi, ImageType.RGB);
|
||||||
|
|
||||||
|
String imageFilename = String.format("page_%03d.png", pageIndex + 1);
|
||||||
|
Path imagePath = tempDir.resolve(imageFilename);
|
||||||
|
|
||||||
|
ImageIO.write(image, "PNG", imagePath.toFile());
|
||||||
|
generatedImages.add(imagePath);
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("Error processing page {}: {}", pageIndex + 1, e.getMessage());
|
||||||
|
} catch (OutOfMemoryError e) {
|
||||||
|
throw ExceptionUtils.createOutOfMemoryDpiException(pageIndex + 1, dpi, e);
|
||||||
|
} catch (NegativeArraySizeException e) {
|
||||||
|
throw ExceptionUtils.createOutOfMemoryDpiException(pageIndex + 1, dpi, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (generatedImages.isEmpty()) {
|
||||||
|
throw new IOException("Failed to render any pages to images for CBR conversion");
|
||||||
|
}
|
||||||
|
|
||||||
|
return createRarArchive(tempDir, generatedImages);
|
||||||
|
} finally {
|
||||||
|
cleanupTempFiles(generatedImages, tempDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] createRarArchive(Path tempDir, List<Path> images) throws IOException {
|
||||||
|
List<String> command = new ArrayList<>();
|
||||||
|
command.add("rar");
|
||||||
|
command.add("a");
|
||||||
|
command.add("-m5");
|
||||||
|
command.add("-ep1");
|
||||||
|
|
||||||
|
Path rarFile = tempDir.resolve("output.cbr");
|
||||||
|
command.add(rarFile.getFileName().toString());
|
||||||
|
|
||||||
|
for (Path image : images) {
|
||||||
|
command.add(image.getFileName().toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
ProcessExecutor executor =
|
||||||
|
ProcessExecutor.getInstance(ProcessExecutor.Processes.INSTALL_APP);
|
||||||
|
try {
|
||||||
|
ProcessExecutorResult result =
|
||||||
|
executor.runCommandWithOutputHandling(command, tempDir.toFile());
|
||||||
|
if (result.getRc() != 0) {
|
||||||
|
throw new IOException("RAR command failed: " + result.getMessages());
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new IOException("RAR command interrupted", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Files.exists(rarFile)) {
|
||||||
|
throw new IOException("RAR file was not created");
|
||||||
|
}
|
||||||
|
|
||||||
|
try (FileInputStream fis = new FileInputStream(rarFile.toFile());
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
|
||||||
|
fis.transferTo(baos);
|
||||||
|
return baos.toByteArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void cleanupTempFiles(List<Path> images, Path tempDir) {
|
||||||
|
for (Path image : images) {
|
||||||
|
try {
|
||||||
|
Files.deleteIfExists(image);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("Failed to delete temp image file {}: {}", image, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (tempDir != null) {
|
||||||
|
try (var paths = Files.walk(tempDir)) {
|
||||||
|
paths.sorted(Comparator.reverseOrder())
|
||||||
|
.forEach(
|
||||||
|
path -> {
|
||||||
|
try {
|
||||||
|
Files.deleteIfExists(path);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn(
|
||||||
|
"Failed to delete temp path {}: {}",
|
||||||
|
path,
|
||||||
|
e.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("Failed to clean up temp directory {}: {}", tempDir, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isPdfFile(MultipartFile file) {
|
||||||
|
String filename = file.getOriginalFilename();
|
||||||
|
if (filename == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String extension = FilenameUtils.getExtension(filename).toLowerCase();
|
||||||
|
return "pdf".equals(extension);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package stirling.software.common.util;
|
||||||
|
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
|
||||||
|
import org.apache.commons.io.FilenameUtils;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.rendering.ImageType;
|
||||||
|
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class PdfToCbzUtils {
|
||||||
|
|
||||||
|
public static byte[] convertPdfToCbz(
|
||||||
|
MultipartFile pdfFile, int dpi, CustomPDFDocumentFactory pdfDocumentFactory)
|
||||||
|
throws IOException {
|
||||||
|
|
||||||
|
validatePdfFile(pdfFile);
|
||||||
|
|
||||||
|
try (PDDocument document = pdfDocumentFactory.load(pdfFile)) {
|
||||||
|
if (document.getNumberOfPages() == 0) {
|
||||||
|
throw new IllegalArgumentException("PDF file contains no pages");
|
||||||
|
}
|
||||||
|
|
||||||
|
return createCbzFromPdf(document, dpi);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validatePdfFile(MultipartFile file) {
|
||||||
|
if (file == null || file.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("File cannot be null or empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
String filename = file.getOriginalFilename();
|
||||||
|
if (filename == null) {
|
||||||
|
throw new IllegalArgumentException("File must have a name");
|
||||||
|
}
|
||||||
|
|
||||||
|
String extension = FilenameUtils.getExtension(filename).toLowerCase();
|
||||||
|
if (!"pdf".equals(extension)) {
|
||||||
|
throw new IllegalArgumentException("File must be a PDF");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] createCbzFromPdf(PDDocument document, int dpi) throws IOException {
|
||||||
|
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
||||||
|
|
||||||
|
try (ByteArrayOutputStream cbzOutputStream = new ByteArrayOutputStream();
|
||||||
|
ZipOutputStream zipOut = new ZipOutputStream(cbzOutputStream)) {
|
||||||
|
|
||||||
|
int totalPages = document.getNumberOfPages();
|
||||||
|
|
||||||
|
for (int pageIndex = 0; pageIndex < totalPages; pageIndex++) {
|
||||||
|
try {
|
||||||
|
BufferedImage image =
|
||||||
|
pdfRenderer.renderImageWithDPI(pageIndex, dpi, ImageType.RGB);
|
||||||
|
|
||||||
|
String imageFilename = String.format("page_%03d.png", pageIndex + 1);
|
||||||
|
|
||||||
|
ZipEntry zipEntry = new ZipEntry(imageFilename);
|
||||||
|
zipOut.putNextEntry(zipEntry);
|
||||||
|
|
||||||
|
ImageIO.write(image, "PNG", zipOut);
|
||||||
|
zipOut.closeEntry();
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("Error processing page {}: {}", pageIndex + 1, e.getMessage());
|
||||||
|
} catch (OutOfMemoryError e) {
|
||||||
|
throw ExceptionUtils.createOutOfMemoryDpiException(pageIndex + 1, dpi, e);
|
||||||
|
} catch (NegativeArraySizeException e) {
|
||||||
|
throw ExceptionUtils.createOutOfMemoryDpiException(pageIndex + 1, dpi, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
zipOut.finish();
|
||||||
|
return cbzOutputStream.toByteArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isPdfFile(MultipartFile file) {
|
||||||
|
String filename = file.getOriginalFilename();
|
||||||
|
if (filename == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String extension = FilenameUtils.getExtension(filename).toLowerCase();
|
||||||
|
return "pdf".equals(extension);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,8 @@ import java.io.IOException;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipOutputStream;
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
@@ -29,19 +31,25 @@ import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
|||||||
import org.apache.pdfbox.rendering.ImageType;
|
import org.apache.pdfbox.rendering.ImageType;
|
||||||
import org.apache.pdfbox.rendering.PDFRenderer;
|
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||||
import org.apache.pdfbox.text.PDFTextStripper;
|
import org.apache.pdfbox.text.PDFTextStripper;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import io.github.pixee.security.Filenames;
|
import io.github.pixee.security.Filenames;
|
||||||
|
|
||||||
|
import lombok.experimental.UtilityClass;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.common.model.ApplicationProperties;
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
@UtilityClass
|
||||||
public class PdfUtils {
|
public class PdfUtils {
|
||||||
|
|
||||||
public static PDRectangle textToPageSize(String size) {
|
private final RegexPatternUtils patternCache = RegexPatternUtils.getInstance();
|
||||||
|
|
||||||
|
public PDRectangle textToPageSize(String size) {
|
||||||
|
|
||||||
switch (size.toUpperCase()) {
|
switch (size.toUpperCase()) {
|
||||||
case "A0" -> {
|
case "A0" -> {
|
||||||
return PDRectangle.A0;
|
return PDRectangle.A0;
|
||||||
@@ -74,7 +82,7 @@ public class PdfUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<RenderedImage> getAllImages(PDResources resources) throws IOException {
|
public List<RenderedImage> getAllImages(PDResources resources) throws IOException {
|
||||||
List<RenderedImage> images = new ArrayList<>();
|
List<RenderedImage> images = new ArrayList<>();
|
||||||
|
|
||||||
for (COSName name : resources.getXObjectNames()) {
|
for (COSName name : resources.getXObjectNames()) {
|
||||||
@@ -91,7 +99,7 @@ public class PdfUtils {
|
|||||||
return images;
|
return images;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean hasImages(PDDocument document, String pagesToCheck) throws IOException {
|
public boolean hasImages(PDDocument document, String pagesToCheck) throws IOException {
|
||||||
String[] pageOrderArr = pagesToCheck.split(",");
|
String[] pageOrderArr = pagesToCheck.split(",");
|
||||||
List<Integer> pageList =
|
List<Integer> pageList =
|
||||||
GeneralUtils.parsePageList(pageOrderArr, document.getNumberOfPages());
|
GeneralUtils.parsePageList(pageOrderArr, document.getNumberOfPages());
|
||||||
@@ -106,7 +114,7 @@ public class PdfUtils {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean hasText(PDDocument document, String pageNumbersToCheck, String phrase)
|
public boolean hasText(PDDocument document, String pageNumbersToCheck, String phrase)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
String[] pageOrderArr = pageNumbersToCheck.split(",");
|
String[] pageOrderArr = pageNumbersToCheck.split(",");
|
||||||
List<Integer> pageList =
|
List<Integer> pageList =
|
||||||
@@ -122,11 +130,11 @@ public class PdfUtils {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean hasImagesOnPage(PDPage page) throws IOException {
|
public boolean hasImagesOnPage(PDPage page) throws IOException {
|
||||||
return getAllImages(page.getResources()).size() > 0;
|
return !getAllImages(page.getResources()).isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean hasTextOnPage(PDPage page, String phrase) throws IOException {
|
public boolean hasTextOnPage(PDPage page, String phrase) throws IOException {
|
||||||
PDFTextStripper textStripper = new PDFTextStripper();
|
PDFTextStripper textStripper = new PDFTextStripper();
|
||||||
PDDocument tempDoc = new PDDocument();
|
PDDocument tempDoc = new PDDocument();
|
||||||
tempDoc.addPage(page);
|
tempDoc.addPage(page);
|
||||||
@@ -135,14 +143,15 @@ public class PdfUtils {
|
|||||||
return pageText.contains(phrase);
|
return pageText.contains(phrase);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static byte[] convertFromPdf(
|
public byte[] convertFromPdf(
|
||||||
CustomPDFDocumentFactory pdfDocumentFactory,
|
CustomPDFDocumentFactory pdfDocumentFactory,
|
||||||
byte[] inputStream,
|
byte[] inputStream,
|
||||||
String imageType,
|
String imageType,
|
||||||
ImageType colorType,
|
ImageType colorType,
|
||||||
boolean singleImage,
|
boolean singleImage,
|
||||||
int DPI,
|
int DPI,
|
||||||
String filename)
|
String filename,
|
||||||
|
boolean includeAnnotations)
|
||||||
throws IOException, Exception {
|
throws IOException, Exception {
|
||||||
|
|
||||||
// Validate and limit DPI to prevent excessive memory usage
|
// Validate and limit DPI to prevent excessive memory usage
|
||||||
@@ -155,7 +164,8 @@ public class PdfUtils {
|
|||||||
if (DPI > maxSafeDpi) {
|
if (DPI > maxSafeDpi) {
|
||||||
throw ExceptionUtils.createIllegalArgumentException(
|
throw ExceptionUtils.createIllegalArgumentException(
|
||||||
"error.dpiExceedsLimit",
|
"error.dpiExceedsLimit",
|
||||||
"DPI value {0} exceeds maximum safe limit of {1}. High DPI values can cause memory issues and crashes. Please use a lower DPI value.",
|
"DPI value {0} exceeds maximum safe limit of {1}. High DPI values can cause"
|
||||||
|
+ " memory issues and crashes. Please use a lower DPI value.",
|
||||||
DPI,
|
DPI,
|
||||||
maxSafeDpi);
|
maxSafeDpi);
|
||||||
}
|
}
|
||||||
@@ -163,6 +173,9 @@ public class PdfUtils {
|
|||||||
try (PDDocument document = pdfDocumentFactory.load(inputStream)) {
|
try (PDDocument document = pdfDocumentFactory.load(inputStream)) {
|
||||||
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
||||||
pdfRenderer.setSubsamplingAllowed(true);
|
pdfRenderer.setSubsamplingAllowed(true);
|
||||||
|
if (!includeAnnotations) {
|
||||||
|
pdfRenderer.setAnnotationsFilter(annotation -> false);
|
||||||
|
}
|
||||||
int pageCount = document.getNumberOfPages();
|
int pageCount = document.getNumberOfPages();
|
||||||
|
|
||||||
// Create a ByteArrayOutputStream to save the image(s) to
|
// Create a ByteArrayOutputStream to save the image(s) to
|
||||||
@@ -192,11 +205,17 @@ public class PdfUtils {
|
|||||||
.contains("Maximum size of image exceeded")) {
|
.contains("Maximum size of image exceeded")) {
|
||||||
throw ExceptionUtils.createIllegalArgumentException(
|
throw ExceptionUtils.createIllegalArgumentException(
|
||||||
"error.pageTooBigForDpi",
|
"error.pageTooBigForDpi",
|
||||||
"PDF page {0} is too large to render at {1} DPI. Please try a lower DPI value (recommended: 150 or less).",
|
"PDF page {0} is too large to render at {1} DPI. Please"
|
||||||
|
+ " try a lower DPI value (recommended: 150 or"
|
||||||
|
+ " less).",
|
||||||
i + 1,
|
i + 1,
|
||||||
DPI);
|
DPI);
|
||||||
}
|
}
|
||||||
throw e;
|
throw e;
|
||||||
|
} catch (OutOfMemoryError e) {
|
||||||
|
throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e);
|
||||||
|
} catch (NegativeArraySizeException e) {
|
||||||
|
throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e);
|
||||||
}
|
}
|
||||||
writer.writeToSequence(new IIOImage(image, null, null), param);
|
writer.writeToSequence(new IIOImage(image, null, null), param);
|
||||||
}
|
}
|
||||||
@@ -237,11 +256,18 @@ public class PdfUtils {
|
|||||||
.contains("Maximum size of image exceeded")) {
|
.contains("Maximum size of image exceeded")) {
|
||||||
throw ExceptionUtils.createIllegalArgumentException(
|
throw ExceptionUtils.createIllegalArgumentException(
|
||||||
"error.pageTooBigExceedsArray",
|
"error.pageTooBigExceedsArray",
|
||||||
"PDF page {0} is too large to render at {1} DPI. The resulting image would exceed Java's maximum array size. Please try a lower DPI value (recommended: 150 or less).",
|
"PDF page {0} is too large to render at {1} DPI. The"
|
||||||
|
+ " resulting image would exceed Java's maximum"
|
||||||
|
+ " array size. Please try a lower DPI value"
|
||||||
|
+ " (recommended: 150 or less).",
|
||||||
i + 1,
|
i + 1,
|
||||||
DPI);
|
DPI);
|
||||||
}
|
}
|
||||||
throw e;
|
throw e;
|
||||||
|
} catch (OutOfMemoryError e) {
|
||||||
|
throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e);
|
||||||
|
} catch (NegativeArraySizeException e) {
|
||||||
|
throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e);
|
||||||
}
|
}
|
||||||
pdfSizeImageIndex = i;
|
pdfSizeImageIndex = i;
|
||||||
dimension =
|
dimension =
|
||||||
@@ -278,11 +304,17 @@ public class PdfUtils {
|
|||||||
.contains("Maximum size of image exceeded")) {
|
.contains("Maximum size of image exceeded")) {
|
||||||
throw ExceptionUtils.createIllegalArgumentException(
|
throw ExceptionUtils.createIllegalArgumentException(
|
||||||
"error.pageTooBigForDpi",
|
"error.pageTooBigForDpi",
|
||||||
"PDF page {0} is too large to render at {1} DPI. Please try a lower DPI value (recommended: 150 or less).",
|
"PDF page {0} is too large to render at {1} DPI. Please"
|
||||||
|
+ " try a lower DPI value (recommended: 150 or"
|
||||||
|
+ " less).",
|
||||||
i + 1,
|
i + 1,
|
||||||
DPI);
|
DPI);
|
||||||
}
|
}
|
||||||
throw e;
|
throw e;
|
||||||
|
} catch (OutOfMemoryError e) {
|
||||||
|
throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e);
|
||||||
|
} catch (NegativeArraySizeException e) {
|
||||||
|
throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,11 +343,16 @@ public class PdfUtils {
|
|||||||
&& e.getMessage().contains("Maximum size of image exceeded")) {
|
&& e.getMessage().contains("Maximum size of image exceeded")) {
|
||||||
throw ExceptionUtils.createIllegalArgumentException(
|
throw ExceptionUtils.createIllegalArgumentException(
|
||||||
"error.pageTooBigForDpi",
|
"error.pageTooBigForDpi",
|
||||||
"PDF page {0} is too large to render at {1} DPI. Please try a lower DPI value (recommended: 150 or less).",
|
"PDF page {0} is too large to render at {1} DPI. Please try"
|
||||||
|
+ " a lower DPI value (recommended: 150 or less).",
|
||||||
i + 1,
|
i + 1,
|
||||||
DPI);
|
DPI);
|
||||||
}
|
}
|
||||||
throw e;
|
throw e;
|
||||||
|
} catch (OutOfMemoryError e) {
|
||||||
|
throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e);
|
||||||
|
} catch (NegativeArraySizeException e) {
|
||||||
|
throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e);
|
||||||
}
|
}
|
||||||
try (ByteArrayOutputStream baosImage = new ByteArrayOutputStream()) {
|
try (ByteArrayOutputStream baosImage = new ByteArrayOutputStream()) {
|
||||||
ImageIO.write(image, imageType, baosImage);
|
ImageIO.write(image, imageType, baosImage);
|
||||||
@@ -349,23 +386,38 @@ public class PdfUtils {
|
|||||||
* @return converted document to PDF-Image
|
* @return converted document to PDF-Image
|
||||||
* @throws IOException if conversion fails
|
* @throws IOException if conversion fails
|
||||||
*/
|
*/
|
||||||
public static PDDocument convertPdfToPdfImage(PDDocument document) throws IOException {
|
public PDDocument convertPdfToPdfImage(PDDocument document) throws IOException {
|
||||||
PDDocument imageDocument = new PDDocument();
|
PDDocument imageDocument = new PDDocument();
|
||||||
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
||||||
pdfRenderer.setSubsamplingAllowed(true);
|
pdfRenderer.setSubsamplingAllowed(true);
|
||||||
for (int page = 0; page < document.getNumberOfPages(); ++page) {
|
for (int page = 0; page < document.getNumberOfPages(); ++page) {
|
||||||
BufferedImage bim;
|
BufferedImage bim;
|
||||||
|
|
||||||
|
// Use global maximum DPI setting, fallback to 300 if not set
|
||||||
|
int renderDpi = 300; // Default fallback
|
||||||
|
ApplicationProperties properties =
|
||||||
|
ApplicationContextProvider.getBean(ApplicationProperties.class);
|
||||||
|
if (properties != null && properties.getSystem() != null) {
|
||||||
|
renderDpi = properties.getSystem().getMaxDPI();
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
bim = pdfRenderer.renderImageWithDPI(page, 300, ImageType.RGB);
|
bim = pdfRenderer.renderImageWithDPI(page, renderDpi, ImageType.RGB);
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
if (e.getMessage() != null
|
if (e.getMessage() != null
|
||||||
&& e.getMessage().contains("Maximum size of image exceeded")) {
|
&& e.getMessage().contains("Maximum size of image exceeded")) {
|
||||||
throw ExceptionUtils.createIllegalArgumentException(
|
throw ExceptionUtils.createIllegalArgumentException(
|
||||||
"error.pageTooBigFor300Dpi",
|
"error.pageTooBigFor300Dpi",
|
||||||
"PDF page {0} is too large to render at 300 DPI. The resulting image would exceed Java's maximum array size. Please use a lower DPI value for PDF-to-image conversion.",
|
"PDF page {0} is too large to render at 300 DPI. The resulting image"
|
||||||
|
+ " would exceed Java's maximum array size. Please use a lower DPI"
|
||||||
|
+ " value for PDF-to-image conversion.",
|
||||||
page + 1);
|
page + 1);
|
||||||
}
|
}
|
||||||
throw e;
|
throw e;
|
||||||
|
} catch (OutOfMemoryError e) {
|
||||||
|
throw ExceptionUtils.createOutOfMemoryDpiException(page + 1, 300, e);
|
||||||
|
} catch (NegativeArraySizeException e) {
|
||||||
|
throw ExceptionUtils.createOutOfMemoryDpiException(page + 1, 300, e);
|
||||||
}
|
}
|
||||||
PDPage originalPage = document.getPage(page);
|
PDPage originalPage = document.getPage(page);
|
||||||
|
|
||||||
@@ -383,8 +435,7 @@ public class PdfUtils {
|
|||||||
return imageDocument;
|
return imageDocument;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static BufferedImage prepareImageForPdfToImage(
|
private BufferedImage prepareImageForPdfToImage(int maxWidth, int height, String imageType) {
|
||||||
int maxWidth, int height, String imageType) {
|
|
||||||
BufferedImage combined;
|
BufferedImage combined;
|
||||||
if ("png".equalsIgnoreCase(imageType)) {
|
if ("png".equalsIgnoreCase(imageType)) {
|
||||||
combined = new BufferedImage(maxWidth, height, BufferedImage.TYPE_INT_ARGB);
|
combined = new BufferedImage(maxWidth, height, BufferedImage.TYPE_INT_ARGB);
|
||||||
@@ -400,7 +451,7 @@ public class PdfUtils {
|
|||||||
return combined;
|
return combined;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static byte[] imageToPdf(
|
public byte[] imageToPdf(
|
||||||
MultipartFile[] files,
|
MultipartFile[] files,
|
||||||
String fitOption,
|
String fitOption,
|
||||||
boolean autoRotate,
|
boolean autoRotate,
|
||||||
@@ -431,7 +482,7 @@ public class PdfUtils {
|
|||||||
ImageProcessingUtils.convertColorType(image, colorType);
|
ImageProcessingUtils.convertColorType(image, colorType);
|
||||||
// Use JPEGFactory if it's JPEG since JPEG is lossy
|
// Use JPEGFactory if it's JPEG since JPEG is lossy
|
||||||
PDImageXObject pdImage =
|
PDImageXObject pdImage =
|
||||||
(contentType != null && "image/jpeg".equals(contentType))
|
(contentType != null && MediaType.IMAGE_JPEG_VALUE.equals(contentType))
|
||||||
? JPEGFactory.createFromImage(doc, convertedImage)
|
? JPEGFactory.createFromImage(doc, convertedImage)
|
||||||
: LosslessFactory.createFromImage(doc, convertedImage);
|
: LosslessFactory.createFromImage(doc, convertedImage);
|
||||||
addImageToDocument(doc, pdImage, fitOption, autoRotate);
|
addImageToDocument(doc, pdImage, fitOption, autoRotate);
|
||||||
@@ -444,7 +495,7 @@ public class PdfUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void addImageToDocument(
|
public void addImageToDocument(
|
||||||
PDDocument doc, PDImageXObject image, String fitOption, boolean autoRotate)
|
PDDocument doc, PDImageXObject image, String fitOption, boolean autoRotate)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
boolean imageIsLandscape = image.getWidth() > image.getHeight();
|
boolean imageIsLandscape = image.getWidth() > image.getHeight();
|
||||||
@@ -494,7 +545,7 @@ public class PdfUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static byte[] overlayImage(
|
public byte[] overlayImage(
|
||||||
CustomPDFDocumentFactory pdfDocumentFactory,
|
CustomPDFDocumentFactory pdfDocumentFactory,
|
||||||
byte[] pdfBytes,
|
byte[] pdfBytes,
|
||||||
byte[] imageBytes,
|
byte[] imageBytes,
|
||||||
@@ -536,13 +587,16 @@ public class PdfUtils {
|
|||||||
public boolean containsTextInFile(PDDocument pdfDocument, String text, String pagesToCheck)
|
public boolean containsTextInFile(PDDocument pdfDocument, String text, String pagesToCheck)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
PDFTextStripper textStripper = new PDFTextStripper();
|
PDFTextStripper textStripper = new PDFTextStripper();
|
||||||
String pdfText = "";
|
StringBuilder pdfText = new StringBuilder();
|
||||||
|
|
||||||
if (pagesToCheck == null || "all".equals(pagesToCheck)) {
|
if (pagesToCheck == null || "all".equals(pagesToCheck)) {
|
||||||
pdfText = textStripper.getText(pdfDocument);
|
pdfText = new StringBuilder(textStripper.getText(pdfDocument));
|
||||||
} else {
|
} else {
|
||||||
// remove whitespaces
|
// remove whitespaces using cached pattern
|
||||||
pagesToCheck = pagesToCheck.replaceAll("\\s+", "");
|
Pattern whitespacePattern =
|
||||||
|
patternCache.getPattern(RegexPatternUtils.getWhitespaceRegex());
|
||||||
|
Matcher whitespaceMatcher = whitespacePattern.matcher(pagesToCheck);
|
||||||
|
pagesToCheck = whitespaceMatcher.replaceAll("");
|
||||||
|
|
||||||
String[] splitPoints = pagesToCheck.split(",");
|
String[] splitPoints = pagesToCheck.split(",");
|
||||||
for (String splitPoint : splitPoints) {
|
for (String splitPoint : splitPoints) {
|
||||||
@@ -555,21 +609,21 @@ public class PdfUtils {
|
|||||||
for (int i = startPage; i <= endPage; i++) {
|
for (int i = startPage; i <= endPage; i++) {
|
||||||
textStripper.setStartPage(i);
|
textStripper.setStartPage(i);
|
||||||
textStripper.setEndPage(i);
|
textStripper.setEndPage(i);
|
||||||
pdfText += textStripper.getText(pdfDocument);
|
pdfText.append(textStripper.getText(pdfDocument));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Handle individual page
|
// Handle individual page
|
||||||
int page = Integer.parseInt(splitPoint);
|
int page = Integer.parseInt(splitPoint);
|
||||||
textStripper.setStartPage(page);
|
textStripper.setStartPage(page);
|
||||||
textStripper.setEndPage(page);
|
textStripper.setEndPage(page);
|
||||||
pdfText += textStripper.getText(pdfDocument);
|
pdfText.append(textStripper.getText(pdfDocument));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pdfDocument.close();
|
pdfDocument.close();
|
||||||
|
|
||||||
return pdfText.contains(text);
|
return pdfText.toString().contains(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean pageCount(PDDocument pdfDocument, int pageCount, String comparator)
|
public boolean pageCount(PDDocument pdfDocument, int pageCount, String comparator)
|
||||||
@@ -577,16 +631,13 @@ public class PdfUtils {
|
|||||||
int actualPageCount = pdfDocument.getNumberOfPages();
|
int actualPageCount = pdfDocument.getNumberOfPages();
|
||||||
pdfDocument.close();
|
pdfDocument.close();
|
||||||
|
|
||||||
switch (comparator.toLowerCase()) {
|
return switch (comparator.toLowerCase()) {
|
||||||
case "greater":
|
case "greater" -> actualPageCount > pageCount;
|
||||||
return actualPageCount > pageCount;
|
case "equal" -> actualPageCount == pageCount;
|
||||||
case "equal":
|
case "less" -> actualPageCount < pageCount;
|
||||||
return actualPageCount == pageCount;
|
default ->
|
||||||
case "less":
|
|
||||||
return actualPageCount < pageCount;
|
|
||||||
default:
|
|
||||||
throw ExceptionUtils.createInvalidArgumentException("comparator", comparator);
|
throw ExceptionUtils.createInvalidArgumentException("comparator", comparator);
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean pageSize(PDDocument pdfDocument, String expectedPageSize) throws IOException {
|
public boolean pageSize(PDDocument pdfDocument, String expectedPageSize) throws IOException {
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import java.util.concurrent.TimeUnit;
|
|||||||
|
|
||||||
import io.github.pixee.security.BoundedLineReader;
|
import io.github.pixee.security.BoundedLineReader;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.common.model.ApplicationProperties;
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
@@ -165,7 +167,7 @@ public class ProcessExecutor {
|
|||||||
semaphore.acquire();
|
semaphore.acquire();
|
||||||
try {
|
try {
|
||||||
|
|
||||||
log.info("Running command: " + String.join(" ", command));
|
log.info("Running command: {}", String.join(" ", command));
|
||||||
ProcessBuilder processBuilder = new ProcessBuilder(command);
|
ProcessBuilder processBuilder = new ProcessBuilder(command);
|
||||||
|
|
||||||
// Use the working directory if it's set
|
// Use the working directory if it's set
|
||||||
@@ -250,7 +252,7 @@ public class ProcessExecutor {
|
|||||||
String outputMessage = String.join("\n", outputLines);
|
String outputMessage = String.join("\n", outputLines);
|
||||||
messages += outputMessage;
|
messages += outputMessage;
|
||||||
if (!liveUpdates) {
|
if (!liveUpdates) {
|
||||||
log.info("Command output:\n" + outputMessage);
|
log.info("Command output:\n{}", outputMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,7 +260,7 @@ public class ProcessExecutor {
|
|||||||
String errorMessage = String.join("\n", errorLines);
|
String errorMessage = String.join("\n", errorLines);
|
||||||
messages += errorMessage;
|
messages += errorMessage;
|
||||||
if (!liveUpdates) {
|
if (!liveUpdates) {
|
||||||
log.warn("Command error output:\n" + errorMessage);
|
log.warn("Command error output:\n{}", errorMessage);
|
||||||
}
|
}
|
||||||
if (exitCode != 0) {
|
if (exitCode != 0) {
|
||||||
if (isQpdf && exitCode == 3) {
|
if (isQpdf && exitCode == 3) {
|
||||||
@@ -303,6 +305,8 @@ public class ProcessExecutor {
|
|||||||
OCR_MY_PDF
|
OCR_MY_PDF
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@Getter
|
||||||
public class ProcessExecutorResult {
|
public class ProcessExecutorResult {
|
||||||
int rc;
|
int rc;
|
||||||
String messages;
|
String messages;
|
||||||
@@ -311,21 +315,5 @@ public class ProcessExecutor {
|
|||||||
this.rc = rc;
|
this.rc = rc;
|
||||||
this.messages = messages;
|
this.messages = messages;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getRc() {
|
|
||||||
return rc;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setRc(int rc) {
|
|
||||||
this.rc = rc;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getMessages() {
|
|
||||||
return messages;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setMessages(String messages) {
|
|
||||||
this.messages = messages;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,524 @@
|
|||||||
|
package stirling.software.common.util;
|
||||||
|
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.regex.PatternSyntaxException;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public final class RegexPatternUtils {
|
||||||
|
|
||||||
|
private static final RegexPatternUtils INSTANCE = new RegexPatternUtils();
|
||||||
|
private final ConcurrentHashMap<PatternKey, Pattern> patternCache = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private static final String WHITESPACE_REGEX = "\\s++";
|
||||||
|
private static final String EXTENSION_REGEX = "\\.(?:[^.]*+)?$";
|
||||||
|
|
||||||
|
private RegexPatternUtils() {
|
||||||
|
super();
|
||||||
|
// Initialize with commonly used patterns for immediate availability
|
||||||
|
precompileCommonPatterns();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the singleton instance of the pattern cache.
|
||||||
|
*
|
||||||
|
* @return the singleton RegexPatternCache instance
|
||||||
|
*/
|
||||||
|
public static RegexPatternUtils getInstance() {
|
||||||
|
return INSTANCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a compiled pattern from cache, compiling and caching if not present.
|
||||||
|
*
|
||||||
|
* <p>This method is thread-safe and uses lazy initialization. Multiple threads calling with the
|
||||||
|
* same regex will result in only one compilation, with all threads receiving the same cached
|
||||||
|
* Pattern instance.
|
||||||
|
*
|
||||||
|
* <p>Performance: first call compiles and caches (expensive), subsequent calls return cached
|
||||||
|
* pattern (fast O(1) lookup).
|
||||||
|
*
|
||||||
|
* @param regex the regular expression string to compile
|
||||||
|
* @return compiled Pattern object, never null
|
||||||
|
* @throws PatternSyntaxException if the regex syntax is invalid
|
||||||
|
* @throws IllegalArgumentException if regex is null
|
||||||
|
*/
|
||||||
|
public Pattern getPattern(String regex) {
|
||||||
|
if (regex == null) {
|
||||||
|
throw new IllegalArgumentException("Regex pattern cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
return patternCache.computeIfAbsent(new PatternKey(regex, 0), this::compilePattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a compiled pattern with flags.
|
||||||
|
*
|
||||||
|
* <p>Patterns with different flags are cached separately using a composite key. Common flags
|
||||||
|
* include:
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link Pattern#CASE_INSENSITIVE} - ignore case differences
|
||||||
|
* <li>{@link Pattern#MULTILINE} - ^ and $ match line boundaries
|
||||||
|
* <li>{@link Pattern#DOTALL} - . matches any character including newlines
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @param regex the regular expression string
|
||||||
|
* @param flags pattern flags (e.g., Pattern.CASE_INSENSITIVE)
|
||||||
|
* @return compiled Pattern object with specified flags
|
||||||
|
* @throws PatternSyntaxException if the regex syntax is invalid
|
||||||
|
* @throws IllegalArgumentException if regex is null
|
||||||
|
*/
|
||||||
|
public Pattern getPattern(String regex, int flags) {
|
||||||
|
if (regex == null) {
|
||||||
|
throw new IllegalArgumentException("Regex pattern cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
return patternCache.computeIfAbsent(new PatternKey(regex, flags), this::compilePattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a pattern is already cached.
|
||||||
|
*
|
||||||
|
* @param regex the regular expression string
|
||||||
|
* @return true if pattern is cached, false otherwise
|
||||||
|
*/
|
||||||
|
public boolean isCached(String regex) {
|
||||||
|
return isCached(regex, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a pattern with flags is already cached.
|
||||||
|
*
|
||||||
|
* @param regex the regular expression string
|
||||||
|
* @param flags pattern flags
|
||||||
|
* @return true if pattern is cached, false otherwise
|
||||||
|
*/
|
||||||
|
public boolean isCached(String regex, int flags) {
|
||||||
|
return regex != null && patternCache.containsKey(new PatternKey(regex, flags));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current cache size (number of cached patterns). Useful for monitoring and debugging.
|
||||||
|
*
|
||||||
|
* @return number of patterns currently cached
|
||||||
|
*/
|
||||||
|
public int getCacheSize() {
|
||||||
|
return patternCache.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all cached patterns. Use sparingly as it forces recompilation of all patterns. Mainly
|
||||||
|
* useful for testing or memory cleanup in long-running applications.
|
||||||
|
*/
|
||||||
|
public void clearCache() {
|
||||||
|
patternCache.clear();
|
||||||
|
log.debug("Regex pattern cache cleared");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a specific pattern from cache.
|
||||||
|
*
|
||||||
|
* @param regex the regular expression string to remove
|
||||||
|
* @return true if pattern was cached and removed, false otherwise
|
||||||
|
*/
|
||||||
|
public boolean removeFromCache(String regex) {
|
||||||
|
return removeFromCache(regex, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a specific pattern with flags from cache.
|
||||||
|
*
|
||||||
|
* @param regex the regular expression string to remove
|
||||||
|
* @param flags pattern flags
|
||||||
|
* @return true if pattern was cached and removed, false otherwise
|
||||||
|
*/
|
||||||
|
public boolean removeFromCache(String regex, int flags) {
|
||||||
|
if (regex == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
PatternKey key = new PatternKey(regex, flags);
|
||||||
|
boolean removed = patternCache.remove(key) != null;
|
||||||
|
if (removed) {
|
||||||
|
log.debug("Removed regex pattern from cache: {} (flags: {})", regex, flags);
|
||||||
|
}
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal method to compile a pattern and handle errors consistently.
|
||||||
|
*
|
||||||
|
* @return compiled Pattern
|
||||||
|
* @throws PatternSyntaxException if regex is invalid
|
||||||
|
*/
|
||||||
|
private Pattern compilePattern(PatternKey key) {
|
||||||
|
String regex = key.regex;
|
||||||
|
int flags = key.flags;
|
||||||
|
|
||||||
|
try {
|
||||||
|
Pattern pattern = Pattern.compile(regex, flags);
|
||||||
|
log.trace("Compiled and cached regex pattern with flags {}: {}", flags, regex);
|
||||||
|
return pattern;
|
||||||
|
} catch (PatternSyntaxException e) {
|
||||||
|
log.error(
|
||||||
|
"Invalid regex pattern: '{}' with flags {} - {}", regex, flags, e.getMessage());
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getWhitespaceRegex() {
|
||||||
|
return WHITESPACE_REGEX;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Creates a case-insensitive pattern for text searching */
|
||||||
|
public Pattern createSearchPattern(String regex, boolean caseInsensitive) {
|
||||||
|
int flags = caseInsensitive ? (Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE) : 0;
|
||||||
|
return getPattern(regex, flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for matching trailing slashes (e.g., "/path/to/dir///") */
|
||||||
|
public Pattern getTrailingSlashesPattern() {
|
||||||
|
return getPattern("/+$");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for removing drive letters from paths */
|
||||||
|
public Pattern getDriveLetterPattern() {
|
||||||
|
return getPattern("^[a-zA-Z]:[\\\\/]+");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for removing leading slashes from paths */
|
||||||
|
public Pattern getLeadingSlashesPattern() {
|
||||||
|
return getPattern("^[\\\\/]+");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for matching backslashes */
|
||||||
|
public Pattern getBackslashPattern() {
|
||||||
|
return getPattern("\\\\");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for sanitizing filenames by removing problematic characters */
|
||||||
|
public Pattern getSafeFilenamePattern() {
|
||||||
|
return getPattern("[/\\\\?%*:|\"<>]");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for sanitizing filenames (keeps only alphanumeric) */
|
||||||
|
public Pattern getFilenameSafePattern() {
|
||||||
|
return getPattern("[^a-zA-Z0-9]");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pattern for replacing non-alphanumeric characters with underscore (explicit underscore
|
||||||
|
* variant)
|
||||||
|
*/
|
||||||
|
public Pattern getNonAlnumUnderscorePattern() {
|
||||||
|
return getPattern("[^A-Za-z0-9_]");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for collapsing multiple underscores */
|
||||||
|
public Pattern getMultipleUnderscoresPattern() {
|
||||||
|
return getPattern("_+");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for trimming leading underscores */
|
||||||
|
public Pattern getLeadingUnderscoresPattern() {
|
||||||
|
return getPattern("^_+");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for trimming trailing underscores */
|
||||||
|
public Pattern getTrailingUnderscoresPattern() {
|
||||||
|
return getPattern("_+$");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for matching upload/download paths (case insensitive) */
|
||||||
|
public Pattern getUploadDownloadPathPattern() {
|
||||||
|
return getPattern("(?i).*/(upload|download)/.*");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for matching one or more whitespace characters */
|
||||||
|
public Pattern getWhitespacePattern() {
|
||||||
|
return getPattern("\\s+");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for matching newlines (Windows and Unix style) */
|
||||||
|
public Pattern getNewlinesPattern() {
|
||||||
|
return getPattern("\\r?\\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for splitting on newlines (Windows and Unix style) */
|
||||||
|
public Pattern getNewlineSplitPattern() {
|
||||||
|
return getPattern("\\r?\\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for splitting text into words */
|
||||||
|
public Pattern getWordSplitPattern() {
|
||||||
|
return getPattern("\\s+");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for removing carriage returns */
|
||||||
|
public Pattern getCarriageReturnPattern() {
|
||||||
|
return getPattern("\\r");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for matching newline characters */
|
||||||
|
public Pattern getNewlineCharsPattern() {
|
||||||
|
return getPattern("[\n\r]");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for multi-format newline splitting (Windows, Mac, Unix) */
|
||||||
|
public Pattern getMultiFormatNewlinePattern() {
|
||||||
|
return getPattern("\r\n|\r|\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for encoded payload newline removal */
|
||||||
|
public Pattern getEncodedPayloadNewlinePattern() {
|
||||||
|
return getPattern("\\r?\\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for escaped newlines in watermark text */
|
||||||
|
public Pattern getEscapedNewlinePattern() {
|
||||||
|
return getPattern("\\\\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for input sanitization (allows only alphanumeric and spaces) */
|
||||||
|
public Pattern getInputSanitizePattern() {
|
||||||
|
return getPattern("[^a-zA-Z0-9 ]");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for removing angle brackets */
|
||||||
|
public Pattern getAngleBracketsPattern() {
|
||||||
|
return getPattern("[<>]");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for removing leading and trailing quotes */
|
||||||
|
public Pattern getQuotesRemovalPattern() {
|
||||||
|
return getPattern("^\"|\"$");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for plus signs (URL encoding replacement) */
|
||||||
|
public Pattern getPlusSignPattern() {
|
||||||
|
return getPattern("\\+");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for username validation */
|
||||||
|
public Pattern getUsernameValidationPattern() {
|
||||||
|
return getPattern("^[a-zA-Z0-9](?!.*[-@._+]{2,})[a-zA-Z0-9@._+-]{1,48}[a-zA-Z0-9]$");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getExtensionRegex() {
|
||||||
|
return EXTENSION_REGEX;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for extracting non-numeric characters */
|
||||||
|
public Pattern getNumericExtractionPattern() {
|
||||||
|
return getPattern("\\D");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for removing non-digit/dot characters (for timeout parsing) */
|
||||||
|
public Pattern getNonDigitDotPattern() {
|
||||||
|
return getPattern("[^\\d.]");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for matching digit/dot characters (for timeout parsing) */
|
||||||
|
public Pattern getDigitDotPattern() {
|
||||||
|
return getPattern("[\\d.]");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for detecting strings containing digits */
|
||||||
|
public Pattern getContainsDigitsPattern() {
|
||||||
|
return getPattern(".*\\d+.*");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for matching 1-3 digit numbers */
|
||||||
|
public Pattern getNumberRangePattern() {
|
||||||
|
return getPattern("[1-9][0-9]{0,2}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for validating mathematical expressions */
|
||||||
|
public Pattern getMathExpressionPattern() {
|
||||||
|
return getPattern("[0-9n+\\-*/() ]+");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for adding multiplication between numbers and 'n' */
|
||||||
|
public Pattern getNumberBeforeNPattern() {
|
||||||
|
return getPattern("(\\d)n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for detecting consecutive 'n' characters */
|
||||||
|
public Pattern getConsecutiveNPattern() {
|
||||||
|
return getPattern(".*n{2,}.*");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for replacing consecutive 'n' characters */
|
||||||
|
public Pattern getConsecutiveNReplacementPattern() {
|
||||||
|
return getPattern("(?<!n)n{2}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for validating HTTP/HTTPS URLs */
|
||||||
|
public Pattern getHttpUrlPattern() {
|
||||||
|
return getPattern("^https?://.*");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for matching URLs in text for link creation */
|
||||||
|
public Pattern getUrlLinkPattern() {
|
||||||
|
return getPattern("(https?://[\\w\\-._~:/?#\\[\\]@!$&'()*+,;=%]+)");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for matching email addresses in text for link creation */
|
||||||
|
public Pattern getEmailLinkPattern() {
|
||||||
|
return getPattern("([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,63})");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for removing script tags from HTML */
|
||||||
|
public Pattern getScriptTagPattern() {
|
||||||
|
return getPattern("(?i)<script[^>]*>.*?</script>");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for removing style tags from HTML */
|
||||||
|
public Pattern getStyleTagPattern() {
|
||||||
|
return getPattern("(?i)<style[^>]*>.*?</style>");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for removing fixed position CSS */
|
||||||
|
public Pattern getFixedPositionCssPattern() {
|
||||||
|
return getPattern("(?i)\\s*position\\s*:\\s*fixed[^;]*;?");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for removing absolute position CSS */
|
||||||
|
public Pattern getAbsolutePositionCssPattern() {
|
||||||
|
return getPattern("(?i)\\s*position\\s*:\\s*absolute[^;]*;?");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for matching size unit suffixes (KB, MB, GB, etc.) */
|
||||||
|
public Pattern getSizeUnitPattern() {
|
||||||
|
return getPattern("[KMGkmg][Bb]");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for system temp file type 1 */
|
||||||
|
public Pattern getSystemTempFile1Pattern() {
|
||||||
|
return getPattern("lu\\d+[a-z0-9]*\\.tmp");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for system temp file type 2 (OCR processes) */
|
||||||
|
public Pattern getSystemTempFile2Pattern() {
|
||||||
|
return getPattern("ocr_process\\d+");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for splitting on whitespace and parentheses */
|
||||||
|
public Pattern getWhitespaceParenthesesSplitPattern() {
|
||||||
|
return getPattern("[\\s\\(\\)]+");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for MIME header whitespace cleanup before encoded sequences */
|
||||||
|
public Pattern getMimeHeaderWhitespacePattern() {
|
||||||
|
return getPattern("\\s+(?==\\?)");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for font name validation (6 uppercase letters + plus + rest) */
|
||||||
|
public Pattern getFontNamePattern() {
|
||||||
|
return getPattern("^[A-Z]{6}\\+.*");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for matching access="readOnly" attribute in XFA XML (with optional whitespace) */
|
||||||
|
public Pattern getAccessReadOnlyPattern() {
|
||||||
|
return getPattern("access\\s*=\\s*\"readOnly\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for matching MIME encoded-word headers (RFC 2047) Example: =?charset?B?encoded?= */
|
||||||
|
public Pattern getMimeEncodedWordPattern() {
|
||||||
|
return getPattern("=\\?([^?]+)\\?([BbQq])\\?([^?]*)\\?=");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for matching inline CID images in HTML (case-insensitive) */
|
||||||
|
public Pattern getInlineCidImagePattern() {
|
||||||
|
return getPattern(
|
||||||
|
"(?i)<img[^>]*\\ssrc\\s*=\\s*['\"]cid:([^'\"]+)['\"][^>]*>",
|
||||||
|
Pattern.CASE_INSENSITIVE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for matching image file extensions (case-insensitive) */
|
||||||
|
public Pattern getImageFilePattern() {
|
||||||
|
return getPattern(".*\\.(jpg|jpeg|png|gif|bmp|webp)$", Pattern.CASE_INSENSITIVE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for matching attachment section headers (case-insensitive) */
|
||||||
|
public Pattern getAttachmentSectionPattern() {
|
||||||
|
return getPattern("attachments\\s*\\(\\d+\\)", Pattern.CASE_INSENSITIVE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for matching filenames in attachment markers */
|
||||||
|
public Pattern getAttachmentFilenamePattern() {
|
||||||
|
return getPattern("@\\s*([^\\s\\(]+(?:\\.[a-zA-Z0-9]+)?)");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for matching pdfaid:part attribute in XMP metadata */
|
||||||
|
public Pattern getPdfAidPartPattern() {
|
||||||
|
return getPattern("pdfaid:part[\"\\s]*=[\"\\s]*([0-9]+)");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for matching pdfaid:conformance attribute in XMP metadata */
|
||||||
|
public Pattern getPdfAidConformancePattern() {
|
||||||
|
return getPattern("pdfaid:conformance[\"\\s]*=[\"\\s]*([A-Za-z]+)");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for matching slash in page mode description */
|
||||||
|
public Pattern getPageModePattern() {
|
||||||
|
return getPattern("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pre-compile commonly used patterns for immediate availability. This eliminates first-call
|
||||||
|
* compilation overhead for frequent patterns.
|
||||||
|
*/
|
||||||
|
private void precompileCommonPatterns() {
|
||||||
|
getPattern("\\.(?:[^.]*+)?$"); // Extension removal - possessive, optional, anchored
|
||||||
|
getPattern("\\.[^.]+$"); // Simple extension match - anchored
|
||||||
|
|
||||||
|
getPattern("\\s+"); // One or more whitespace
|
||||||
|
getPattern("\\s*"); // Zero or more whitespace
|
||||||
|
|
||||||
|
getPattern("/+$"); // Trailing slashes
|
||||||
|
getPattern("\\D"); // Non-numeric characters
|
||||||
|
getPattern("[/\\\\?%*:|\"<>]"); // Unsafe filename characters
|
||||||
|
getPattern("[^a-zA-Z0-9 ]"); // Input sanitization
|
||||||
|
getPattern("[^a-zA-Z0-9]"); // Filename sanitization
|
||||||
|
// API doc patterns
|
||||||
|
getPattern("Output:(\\w+)"); // precompiled single-escaped for runtime regex \w
|
||||||
|
getPattern("Input:(\\w+)");
|
||||||
|
getPattern("Type:(\\w+)");
|
||||||
|
log.debug("Pre-compiled {} common regex patterns", patternCache.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for email validation */
|
||||||
|
public Pattern getEmailValidationPattern() {
|
||||||
|
return getPattern(
|
||||||
|
"^(?=.{1,320}$)(?=.{1,64}@)[A-Za-z0-9](?:[A-Za-z0-9_.+-]*[A-Za-z0-9])?@[^-][A-Za-z0-9-]+(?:\\.[A-Za-z0-9-]+)*(?:\\.[A-Za-z]{2,})$");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pattern for matching Output:<TYPE> in API descriptions */
|
||||||
|
public Pattern getApiDocOutputTypePattern() {
|
||||||
|
return getPattern("Output:(\\w+)");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pattern for matching Input:<TYPE> in API descriptions */
|
||||||
|
public Pattern getApiDocInputTypePattern() {
|
||||||
|
return getPattern("Input:(\\w+)");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pattern for matching Type:<CODE> in API descriptions
|
||||||
|
*/
|
||||||
|
public Pattern getApiDocTypePattern() {
|
||||||
|
return getPattern("Type:(\\w+)");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pattern for validating file extensions (2-4 alphanumeric, case-insensitive) */
|
||||||
|
public Pattern getFileExtensionValidationPattern() {
|
||||||
|
return getPattern("^[a-zA-Z0-9]{2,4}$", Pattern.CASE_INSENSITIVE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record PatternKey(String regex, int flags) {
|
||||||
|
// Record automatically provides equals, hashCode, and toString
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import java.io.File;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -14,17 +15,13 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
public class TempFile implements AutoCloseable {
|
public class TempFile implements AutoCloseable {
|
||||||
|
|
||||||
private final TempFileManager manager;
|
private final TempFileManager manager;
|
||||||
private final File file;
|
@Getter private final File file;
|
||||||
|
|
||||||
public TempFile(TempFileManager manager, String suffix) throws IOException {
|
public TempFile(TempFileManager manager, String suffix) throws IOException {
|
||||||
this.manager = manager;
|
this.manager = manager;
|
||||||
this.file = manager.createTempFile(suffix);
|
this.file = manager.createTempFile(suffix);
|
||||||
}
|
}
|
||||||
|
|
||||||
public File getFile() {
|
|
||||||
return file;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Path getPath() {
|
public Path getPath() {
|
||||||
return file.toPath();
|
return file.toPath();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import java.util.stream.Collectors;
|
|||||||
|
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -24,8 +25,22 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
public class TempFileRegistry {
|
public class TempFileRegistry {
|
||||||
|
|
||||||
private final ConcurrentMap<Path, Instant> registeredFiles = new ConcurrentHashMap<>();
|
private final ConcurrentMap<Path, Instant> registeredFiles = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* -- GETTER -- Get all registered third-party temporary files.
|
||||||
|
*
|
||||||
|
* @return Set of third-party file paths
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
private final Set<Path> thirdPartyTempFiles =
|
private final Set<Path> thirdPartyTempFiles =
|
||||||
Collections.newSetFromMap(new ConcurrentHashMap<>());
|
Collections.newSetFromMap(new ConcurrentHashMap<>());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* -- GETTER -- Get all registered temporary directories.
|
||||||
|
*
|
||||||
|
* @return Set of temporary directory paths
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
private final Set<Path> tempDirectories = Collections.newSetFromMap(new ConcurrentHashMap<>());
|
private final Set<Path> tempDirectories = Collections.newSetFromMap(new ConcurrentHashMap<>());
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -133,24 +148,6 @@ public class TempFileRegistry {
|
|||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all registered third-party temporary files.
|
|
||||||
*
|
|
||||||
* @return Set of third-party file paths
|
|
||||||
*/
|
|
||||||
public Set<Path> getThirdPartyTempFiles() {
|
|
||||||
return thirdPartyTempFiles;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all registered temporary directories.
|
|
||||||
*
|
|
||||||
* @return Set of temporary directory paths
|
|
||||||
*/
|
|
||||||
public Set<Path> getTempDirectories() {
|
|
||||||
return tempDirectories;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a file is registered in the registry.
|
* Check if a file is registered in the registry.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import java.io.ByteArrayOutputStream;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
@@ -11,9 +13,13 @@ import org.springframework.http.HttpStatus;
|
|||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
|
||||||
|
|
||||||
import io.github.pixee.security.Filenames;
|
import io.github.pixee.security.Filenames;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
public class WebResponseUtils {
|
public class WebResponseUtils {
|
||||||
|
|
||||||
public static ResponseEntity<byte[]> baosToWebResponse(
|
public static ResponseEntity<byte[]> baosToWebResponse(
|
||||||
@@ -44,7 +50,10 @@ public class WebResponseUtils {
|
|||||||
headers.setContentType(mediaType);
|
headers.setContentType(mediaType);
|
||||||
headers.setContentLength(bytes.length);
|
headers.setContentLength(bytes.length);
|
||||||
String encodedDocName =
|
String encodedDocName =
|
||||||
URLEncoder.encode(docName, StandardCharsets.UTF_8).replaceAll("\\+", "%20");
|
RegexPatternUtils.getInstance()
|
||||||
|
.getPlusSignPattern()
|
||||||
|
.matcher(URLEncoder.encode(docName, StandardCharsets.UTF_8))
|
||||||
|
.replaceAll("%20");
|
||||||
headers.setContentDispositionFormData("attachment", encodedDocName);
|
headers.setContentDispositionFormData("attachment", encodedDocName);
|
||||||
return new ResponseEntity<>(bytes, headers, HttpStatus.OK);
|
return new ResponseEntity<>(bytes, headers, HttpStatus.OK);
|
||||||
}
|
}
|
||||||
@@ -64,4 +73,59 @@ public class WebResponseUtils {
|
|||||||
|
|
||||||
return baosToWebResponse(baos, docName);
|
return baosToWebResponse(baos, docName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a File to a web response (PDF default).
|
||||||
|
*
|
||||||
|
* @param outputTempFile The temporary file to be sent as a response.
|
||||||
|
* @param docName The name of the document.
|
||||||
|
* @return A ResponseEntity containing the file as a resource.
|
||||||
|
*/
|
||||||
|
public static ResponseEntity<StreamingResponseBody> pdfFileToWebResponse(
|
||||||
|
TempFile outputTempFile, String docName) throws IOException {
|
||||||
|
return fileToWebResponse(outputTempFile, docName, MediaType.APPLICATION_PDF);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a File to a web response (ZIP default).
|
||||||
|
*
|
||||||
|
* @param outputTempFile The temporary file to be sent as a response.
|
||||||
|
* @param docName The name of the document.
|
||||||
|
* @return A ResponseEntity containing the file as a resource.
|
||||||
|
*/
|
||||||
|
public static ResponseEntity<StreamingResponseBody> zipFileToWebResponse(
|
||||||
|
TempFile outputTempFile, String docName) throws IOException {
|
||||||
|
return fileToWebResponse(outputTempFile, docName, MediaType.APPLICATION_OCTET_STREAM);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a File to a web response with explicit media type (e.g., ZIP).
|
||||||
|
*
|
||||||
|
* @param outputTempFile The temporary file to be sent as a response.
|
||||||
|
* @param docName The name of the document.
|
||||||
|
* @param mediaType The content type to set on the response.
|
||||||
|
* @return A ResponseEntity containing the file as a resource.
|
||||||
|
*/
|
||||||
|
public static ResponseEntity<StreamingResponseBody> fileToWebResponse(
|
||||||
|
TempFile outputTempFile, String docName, MediaType mediaType) throws IOException {
|
||||||
|
|
||||||
|
Path path = outputTempFile.getFile().toPath().normalize();
|
||||||
|
long len = Files.size(path);
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setContentType(mediaType);
|
||||||
|
headers.setContentLength(len);
|
||||||
|
headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + docName + "\"");
|
||||||
|
|
||||||
|
StreamingResponseBody body =
|
||||||
|
os -> {
|
||||||
|
try (os) {
|
||||||
|
Files.copy(path, os);
|
||||||
|
os.flush();
|
||||||
|
} finally {
|
||||||
|
outputTempFile.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return new ResponseEntity<>(body, headers, HttpStatus.OK);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+86
@@ -0,0 +1,86 @@
|
|||||||
|
package stirling.software.common.util.misc;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.core.io.InputStreamResource;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import stirling.software.common.model.api.misc.ReplaceAndInvert;
|
||||||
|
import stirling.software.common.util.ProcessExecutor;
|
||||||
|
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
||||||
|
import stirling.software.common.util.TempFile;
|
||||||
|
import stirling.software.common.util.TempFileManager;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class ColorSpaceConversionStrategy extends ReplaceAndInvertColorStrategy {
|
||||||
|
|
||||||
|
private final TempFileManager tempFileManager;
|
||||||
|
|
||||||
|
public ColorSpaceConversionStrategy(
|
||||||
|
MultipartFile file,
|
||||||
|
ReplaceAndInvert replaceAndInvert,
|
||||||
|
TempFileManager tempFileManager) {
|
||||||
|
super(file, replaceAndInvert);
|
||||||
|
this.tempFileManager = tempFileManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputStreamResource replace() throws IOException {
|
||||||
|
try (TempFile tempInput = new TempFile(tempFileManager, ".pdf");
|
||||||
|
TempFile tempOutput = new TempFile(tempFileManager, ".pdf")) {
|
||||||
|
|
||||||
|
Path tempInputFile = tempInput.getPath();
|
||||||
|
Path tempOutputFile = tempOutput.getPath();
|
||||||
|
|
||||||
|
Files.write(tempInputFile, getFileInput().getBytes());
|
||||||
|
|
||||||
|
log.info("Starting CMYK color space conversion");
|
||||||
|
|
||||||
|
List<String> command = new ArrayList<>();
|
||||||
|
command.add("gs");
|
||||||
|
command.add("-sDEVICE=pdfwrite");
|
||||||
|
command.add("-dCompatibilityLevel=1.5");
|
||||||
|
command.add("-dPDFSETTINGS=/prepress");
|
||||||
|
command.add("-dNOPAUSE");
|
||||||
|
command.add("-dQUIET");
|
||||||
|
command.add("-dBATCH");
|
||||||
|
command.add("-sProcessColorModel=DeviceCMYK");
|
||||||
|
command.add("-sColorConversionStrategy=CMYK");
|
||||||
|
command.add("-sColorConversionStrategyForImages=CMYK");
|
||||||
|
command.add("-sOutputFile=" + tempOutputFile.toString());
|
||||||
|
command.add(tempInputFile.toString());
|
||||||
|
|
||||||
|
log.debug("Executing Ghostscript command for CMYK conversion: {}", command);
|
||||||
|
|
||||||
|
ProcessExecutorResult result =
|
||||||
|
ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT)
|
||||||
|
.runCommandWithOutputHandling(command);
|
||||||
|
|
||||||
|
if (result.getRc() != 0) {
|
||||||
|
log.error(
|
||||||
|
"Ghostscript CMYK conversion failed with return code: {}. Output: {}",
|
||||||
|
result.getRc(),
|
||||||
|
result.getMessages());
|
||||||
|
throw new IOException(
|
||||||
|
"CMYK color space conversion failed: " + result.getMessages());
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("CMYK color space conversion completed successfully");
|
||||||
|
|
||||||
|
byte[] pdfBytes = Files.readAllBytes(tempOutputFile);
|
||||||
|
return new InputStreamResource(new ByteArrayInputStream(pdfBytes));
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("CMYK color space conversion failed", e);
|
||||||
|
throw new IOException(
|
||||||
|
"Failed to convert PDF to CMYK color space: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+22
-2
@@ -19,7 +19,10 @@ import org.apache.pdfbox.rendering.PDFRenderer;
|
|||||||
import org.springframework.core.io.InputStreamResource;
|
import org.springframework.core.io.InputStreamResource;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
import stirling.software.common.model.api.misc.ReplaceAndInvert;
|
import stirling.software.common.model.api.misc.ReplaceAndInvert;
|
||||||
|
import stirling.software.common.util.ApplicationContextProvider;
|
||||||
|
import stirling.software.common.util.ExceptionUtils;
|
||||||
|
|
||||||
public class InvertFullColorStrategy extends ReplaceAndInvertColorStrategy {
|
public class InvertFullColorStrategy extends ReplaceAndInvertColorStrategy {
|
||||||
|
|
||||||
@@ -44,8 +47,25 @@ public class InvertFullColorStrategy extends ReplaceAndInvertColorStrategy {
|
|||||||
// Render each page and invert colors
|
// Render each page and invert colors
|
||||||
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
||||||
for (int page = 0; page < document.getNumberOfPages(); page++) {
|
for (int page = 0; page < document.getNumberOfPages(); page++) {
|
||||||
BufferedImage image =
|
BufferedImage image;
|
||||||
pdfRenderer.renderImageWithDPI(page, 300); // Render page at 300 DPI
|
|
||||||
|
// Use global maximum DPI setting, fallback to 300 if not set
|
||||||
|
int renderDpi = 300; // Default fallback
|
||||||
|
ApplicationProperties properties =
|
||||||
|
ApplicationContextProvider.getBean(ApplicationProperties.class);
|
||||||
|
if (properties != null && properties.getSystem() != null) {
|
||||||
|
renderDpi = properties.getSystem().getMaxDPI();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
image =
|
||||||
|
pdfRenderer.renderImageWithDPI(
|
||||||
|
page, renderDpi); // Render page with global DPI setting
|
||||||
|
} catch (OutOfMemoryError e) {
|
||||||
|
throw ExceptionUtils.createOutOfMemoryDpiException(page + 1, renderDpi, e);
|
||||||
|
} catch (NegativeArraySizeException e) {
|
||||||
|
throw ExceptionUtils.createOutOfMemoryDpiException(page + 1, renderDpi, e);
|
||||||
|
}
|
||||||
|
|
||||||
// Invert the colors
|
// Invert the colors
|
||||||
invertImageColors(image);
|
invertImageColors(image);
|
||||||
|
|||||||
+1
-5
@@ -29,7 +29,6 @@ import jakarta.servlet.http.HttpServletRequest;
|
|||||||
|
|
||||||
import stirling.software.common.aop.AutoJobAspect;
|
import stirling.software.common.aop.AutoJobAspect;
|
||||||
import stirling.software.common.model.api.PDFFile;
|
import stirling.software.common.model.api.PDFFile;
|
||||||
import stirling.software.common.service.FileOrUploadService;
|
|
||||||
import stirling.software.common.service.FileStorage;
|
import stirling.software.common.service.FileStorage;
|
||||||
import stirling.software.common.service.JobExecutorService;
|
import stirling.software.common.service.JobExecutorService;
|
||||||
import stirling.software.common.service.JobQueue;
|
import stirling.software.common.service.JobQueue;
|
||||||
@@ -44,8 +43,6 @@ class AutoJobPostMappingIntegrationTest {
|
|||||||
|
|
||||||
@Mock private HttpServletRequest request;
|
@Mock private HttpServletRequest request;
|
||||||
|
|
||||||
@Mock private FileOrUploadService fileOrUploadService;
|
|
||||||
|
|
||||||
@Mock private FileStorage fileStorage;
|
@Mock private FileStorage fileStorage;
|
||||||
|
|
||||||
@Mock private ResourceMonitor resourceMonitor;
|
@Mock private ResourceMonitor resourceMonitor;
|
||||||
@@ -54,8 +51,7 @@ class AutoJobPostMappingIntegrationTest {
|
|||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
autoJobAspect =
|
autoJobAspect = new AutoJobAspect(jobExecutorService, request, fileStorage);
|
||||||
new AutoJobAspect(jobExecutorService, request, fileOrUploadService, fileStorage);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mock private ProceedingJoinPoint joinPoint;
|
@Mock private ProceedingJoinPoint joinPoint;
|
||||||
|
|||||||
+9
-6
@@ -2,9 +2,11 @@ package stirling.software.common.model;
|
|||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
import java.nio.file.Paths;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
@@ -31,22 +33,23 @@ class ApplicationPropertiesLogicTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void tempFileManagement_defaults_and_overrides() {
|
void tempFileManagement_defaults_and_overrides() {
|
||||||
|
Function<String, String> normalize = s -> Paths.get(s).normalize().toString();
|
||||||
ApplicationProperties.TempFileManagement tfm =
|
ApplicationProperties.TempFileManagement tfm =
|
||||||
new ApplicationProperties.TempFileManagement();
|
new ApplicationProperties.TempFileManagement();
|
||||||
|
|
||||||
String expectedBase =
|
String expectedBase =
|
||||||
java.lang.System.getProperty("java.io.tmpdir").replaceAll("/+$", "")
|
Paths.get(java.lang.System.getProperty("java.io.tmpdir"), "stirling-pdf")
|
||||||
+ "/stirling-pdf";
|
.toString();
|
||||||
assertEquals(expectedBase, tfm.getBaseTmpDir());
|
assertEquals(expectedBase, tfm.getBaseTmpDir());
|
||||||
|
|
||||||
String expectedLibre = expectedBase + "/libreoffice";
|
String expectedLibre = Paths.get(expectedBase, "libreoffice").toString();
|
||||||
assertEquals(expectedLibre, tfm.getLibreofficeDir());
|
assertEquals(expectedLibre, tfm.getLibreofficeDir());
|
||||||
|
|
||||||
tfm.setBaseTmpDir("/custom/base");
|
tfm.setBaseTmpDir("/custom/base");
|
||||||
assertEquals("/custom/base", tfm.getBaseTmpDir());
|
assertEquals(normalize.apply("/custom/base"), normalize.apply(tfm.getBaseTmpDir()));
|
||||||
|
|
||||||
tfm.setLibreofficeDir("/opt/libre");
|
tfm.setLibreofficeDir("/opt/libre");
|
||||||
assertEquals("/opt/libre", tfm.getLibreofficeDir());
|
assertEquals(normalize.apply("/opt/libre"), normalize.apply(tfm.getLibreofficeDir()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -155,7 +158,7 @@ class ApplicationPropertiesLogicTest {
|
|||||||
assertEquals(30, t.getOcrMyPdfTimeoutMinutes());
|
assertEquals(30, t.getOcrMyPdfTimeoutMinutes());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Deprecated
|
@Deprecated(since = "0.45.0")
|
||||||
@Test
|
@Test
|
||||||
void enterprise_metadata_defaults() {
|
void enterprise_metadata_defaults() {
|
||||||
ApplicationProperties.EnterpriseEdition ee = new ApplicationProperties.EnterpriseEdition();
|
ApplicationProperties.EnterpriseEdition ee = new ApplicationProperties.EnterpriseEdition();
|
||||||
|
|||||||
+2
-1
@@ -11,6 +11,7 @@ import java.nio.file.Path;
|
|||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.core.io.FileSystemResource;
|
import org.springframework.core.io.FileSystemResource;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
|
||||||
import okhttp3.mockwebserver.MockResponse;
|
import okhttp3.mockwebserver.MockResponse;
|
||||||
import okhttp3.mockwebserver.MockWebServer;
|
import okhttp3.mockwebserver.MockWebServer;
|
||||||
@@ -23,7 +24,7 @@ class ApplicationPropertiesSaml2HttpTest {
|
|||||||
server.enqueue(
|
server.enqueue(
|
||||||
new MockResponse()
|
new MockResponse()
|
||||||
.setResponseCode(200)
|
.setResponseCode(200)
|
||||||
.addHeader("Content-Type", "application/xml")
|
.addHeader("Content-Type", MediaType.APPLICATION_XML_VALUE)
|
||||||
.setBody("<EntityDescriptor/>"));
|
.setBody("<EntityDescriptor/>"));
|
||||||
server.start();
|
server.start();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package stirling.software.common.model;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.CsvSource;
|
||||||
|
|
||||||
|
public class FileInfoTest {
|
||||||
|
|
||||||
|
@ParameterizedTest(name = "{index}: fileSize={0}")
|
||||||
|
@CsvSource({
|
||||||
|
"0, '0 Bytes'",
|
||||||
|
"1023, '1023 Bytes'",
|
||||||
|
"1024, '1.00 KB'",
|
||||||
|
"1048575, '1024.00 KB'", // Do we really want this as result?
|
||||||
|
"1048576, '1.00 MB'",
|
||||||
|
"1073741823, '1024.00 MB'", // Do we really want this as result?
|
||||||
|
"1073741824, '1.00 GB'"
|
||||||
|
})
|
||||||
|
void testGetFormattedFileSize(long fileSize, String expectedFormattedSize) {
|
||||||
|
FileInfo fileInfo =
|
||||||
|
new FileInfo(
|
||||||
|
"example.txt",
|
||||||
|
File.separator
|
||||||
|
+ "path"
|
||||||
|
+ File.separator
|
||||||
|
+ "to"
|
||||||
|
+ File.separator
|
||||||
|
+ "example.txt",
|
||||||
|
LocalDateTime.now(),
|
||||||
|
fileSize,
|
||||||
|
LocalDateTime.now().minusDays(1));
|
||||||
|
|
||||||
|
assertEquals(expectedFormattedSize, fileInfo.getFormattedFileSize());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetFilePathAsPath() {
|
||||||
|
FileInfo fileInfo =
|
||||||
|
new FileInfo(
|
||||||
|
"test.pdf",
|
||||||
|
File.separator + "tmp" + File.separator + "test.pdf",
|
||||||
|
LocalDateTime.now(),
|
||||||
|
1234,
|
||||||
|
LocalDateTime.now().minusDays(2));
|
||||||
|
assertEquals(
|
||||||
|
File.separator + "tmp" + File.separator + "test.pdf",
|
||||||
|
fileInfo.getFilePathAsPath().toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetFormattedModificationDate() {
|
||||||
|
LocalDateTime modDate = LocalDateTime.of(2024, 6, 1, 15, 30, 45);
|
||||||
|
FileInfo fileInfo =
|
||||||
|
new FileInfo(
|
||||||
|
"file.txt",
|
||||||
|
File.separator + "file.txt",
|
||||||
|
modDate,
|
||||||
|
100,
|
||||||
|
LocalDateTime.of(2024, 5, 31, 10, 0, 0));
|
||||||
|
assertEquals("2024-06-01 15:30:45", fileInfo.getFormattedModificationDate());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetFormattedCreationDate() {
|
||||||
|
LocalDateTime creationDate = LocalDateTime.of(2023, 12, 25, 8, 15, 0);
|
||||||
|
FileInfo fileInfo =
|
||||||
|
new FileInfo(
|
||||||
|
"holiday.txt",
|
||||||
|
File.separator + "holiday.txt",
|
||||||
|
LocalDateTime.of(2024, 1, 1, 0, 0, 0),
|
||||||
|
500,
|
||||||
|
creationDate);
|
||||||
|
assertEquals("2023-12-25 08:15:00", fileInfo.getFormattedCreationDate());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGettersAndSetters() {
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
FileInfo fileInfo =
|
||||||
|
new FileInfo(
|
||||||
|
"doc.pdf",
|
||||||
|
File.separator + "docs" + File.separator + "doc.pdf",
|
||||||
|
now,
|
||||||
|
2048,
|
||||||
|
now.minusDays(1));
|
||||||
|
// Test getters
|
||||||
|
assertEquals("doc.pdf", fileInfo.getFileName());
|
||||||
|
assertEquals(File.separator + "docs" + File.separator + "doc.pdf", fileInfo.getFilePath());
|
||||||
|
assertEquals(now, fileInfo.getModificationDate());
|
||||||
|
assertEquals(2048, fileInfo.getFileSize());
|
||||||
|
assertEquals(now.minusDays(1), fileInfo.getCreationDate());
|
||||||
|
|
||||||
|
// Test setters
|
||||||
|
fileInfo.setFileName("new.pdf");
|
||||||
|
fileInfo.setFilePath(File.separator + "new" + File.separator + "new.pdf");
|
||||||
|
fileInfo.setModificationDate(now.plusDays(1));
|
||||||
|
fileInfo.setFileSize(4096);
|
||||||
|
fileInfo.setCreationDate(now.minusDays(2));
|
||||||
|
|
||||||
|
assertEquals("new.pdf", fileInfo.getFileName());
|
||||||
|
assertEquals(File.separator + "new" + File.separator + "new.pdf", fileInfo.getFilePath());
|
||||||
|
assertEquals(now.plusDays(1), fileInfo.getModificationDate());
|
||||||
|
assertEquals(4096, fileInfo.getFileSize());
|
||||||
|
assertEquals(now.minusDays(2), fileInfo.getCreationDate());
|
||||||
|
}
|
||||||
|
}
|
||||||
+88
@@ -0,0 +1,88 @@
|
|||||||
|
package stirling.software.common.model;
|
||||||
|
|
||||||
|
/* Commented out - InputStreamTemplateResource class removed with Thymeleaf migration
|
||||||
|
* This test will be removed when frontend migration to React is complete
|
||||||
|
|
||||||
|
|
||||||
|
public class InputStreamTemplateResourceTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void gettersReturnProvidedFields() {
|
||||||
|
byte[] data = {1, 2, 3};
|
||||||
|
InputStream is = new ByteArrayInputStream(data);
|
||||||
|
String encoding = "UTF-8";
|
||||||
|
InputStreamTemplateResource resource = new InputStreamTemplateResource(is, encoding);
|
||||||
|
|
||||||
|
assertSame(is, resource.getInputStream());
|
||||||
|
assertEquals(encoding, resource.getCharacterEncoding());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void fieldsAreFinal() throws NoSuchFieldException {
|
||||||
|
Field inputStreamField = InputStreamTemplateResource.class.getDeclaredField("inputStream");
|
||||||
|
Field characterEncodingField =
|
||||||
|
InputStreamTemplateResource.class.getDeclaredField("characterEncoding");
|
||||||
|
|
||||||
|
assertTrue(Modifier.isFinal(inputStreamField.getModifiers()));
|
||||||
|
assertTrue(Modifier.isFinal(characterEncodingField.getModifiers()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void noSetterMethodsPresent() {
|
||||||
|
long setterCount =
|
||||||
|
Arrays.stream(InputStreamTemplateResource.class.getDeclaredMethods())
|
||||||
|
.filter(method -> method.getName().startsWith("set"))
|
||||||
|
.count();
|
||||||
|
|
||||||
|
assertEquals(0, setterCount, "InputStreamTemplateResource should not have setter methods");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void readerReturnsCorrectContent() throws Exception {
|
||||||
|
String content = "Hello, world!";
|
||||||
|
InputStream is = new ByteArrayInputStream(content.getBytes("UTF-8"));
|
||||||
|
InputStreamTemplateResource resource = new InputStreamTemplateResource(is, "UTF-8");
|
||||||
|
|
||||||
|
try (Reader reader = resource.reader()) {
|
||||||
|
char[] buffer = new char[content.length()];
|
||||||
|
int read = reader.read(buffer);
|
||||||
|
assertEquals(content.length(), read);
|
||||||
|
assertEquals(content, new String(buffer));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void relativeThrowsUnsupportedOperationException() {
|
||||||
|
InputStream is = new ByteArrayInputStream(new byte[0]);
|
||||||
|
InputStreamTemplateResource resource = new InputStreamTemplateResource(is, "UTF-8");
|
||||||
|
assertThrows(UnsupportedOperationException.class, () -> resource.relative("other"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getDescriptionReturnsExpectedString() {
|
||||||
|
InputStream is = new ByteArrayInputStream(new byte[0]);
|
||||||
|
InputStreamTemplateResource resource = new InputStreamTemplateResource(is, "UTF-8");
|
||||||
|
assertEquals("InputStream resource [Stream]", resource.getDescription());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getBaseNameReturnsExpectedString() {
|
||||||
|
InputStream is = new ByteArrayInputStream(new byte[0]);
|
||||||
|
InputStreamTemplateResource resource = new InputStreamTemplateResource(is, "UTF-8");
|
||||||
|
assertEquals("streamResource", resource.getBaseName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void existsReturnsTrueWhenInputStreamNotNull() {
|
||||||
|
InputStream is = new ByteArrayInputStream(new byte[0]);
|
||||||
|
InputStreamTemplateResource resource = new InputStreamTemplateResource(is, "UTF-8");
|
||||||
|
assertTrue(resource.exists());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void existsReturnsFalseWhenInputStreamIsNull() {
|
||||||
|
InputStreamTemplateResource resource = new InputStreamTemplateResource(null, "UTF-8");
|
||||||
|
assertFalse(resource.exists());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
+5
-3
@@ -1,12 +1,12 @@
|
|||||||
package stirling.software.common.service;
|
package stirling.software.common.service;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.Mockito.*;
|
import static org.mockito.Mockito.*;
|
||||||
import static stirling.software.common.service.SpyPDFDocumentFactory.*;
|
import static stirling.software.common.service.SpyPDFDocumentFactory.*;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.nio.file.*;
|
import java.nio.file.*;
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
import org.apache.pdfbox.Loader;
|
import org.apache.pdfbox.Loader;
|
||||||
@@ -18,9 +18,11 @@ import org.junit.jupiter.api.parallel.Execution;
|
|||||||
import org.junit.jupiter.api.parallel.ExecutionMode;
|
import org.junit.jupiter.api.parallel.ExecutionMode;
|
||||||
import org.junit.jupiter.params.ParameterizedTest;
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
import org.junit.jupiter.params.provider.CsvSource;
|
import org.junit.jupiter.params.provider.CsvSource;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.mock.web.MockMultipartFile;
|
import org.springframework.mock.web.MockMultipartFile;
|
||||||
|
|
||||||
import stirling.software.common.model.api.PDFFile;
|
import stirling.software.common.model.api.PDFFile;
|
||||||
|
import stirling.software.common.service.SpyPDFDocumentFactory.StrategyType;
|
||||||
|
|
||||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||||
@@ -73,7 +75,7 @@ class CustomPDFDocumentFactoryTest {
|
|||||||
void testStrategy_MultipartFile(int sizeMB, StrategyType expected) throws IOException {
|
void testStrategy_MultipartFile(int sizeMB, StrategyType expected) throws IOException {
|
||||||
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
|
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
|
||||||
MockMultipartFile multipart =
|
MockMultipartFile multipart =
|
||||||
new MockMultipartFile("file", "doc.pdf", "application/pdf", inflated);
|
new MockMultipartFile("file", "doc.pdf", MediaType.APPLICATION_PDF_VALUE, inflated);
|
||||||
try (PDDocument doc = factory.load(multipart)) {
|
try (PDDocument doc = factory.load(multipart)) {
|
||||||
Assertions.assertEquals(expected, factory.lastStrategyUsed);
|
Assertions.assertEquals(expected, factory.lastStrategyUsed);
|
||||||
}
|
}
|
||||||
@@ -84,7 +86,7 @@ class CustomPDFDocumentFactoryTest {
|
|||||||
void testStrategy_PDFFile(int sizeMB, StrategyType expected) throws IOException {
|
void testStrategy_PDFFile(int sizeMB, StrategyType expected) throws IOException {
|
||||||
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
|
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
|
||||||
MockMultipartFile multipart =
|
MockMultipartFile multipart =
|
||||||
new MockMultipartFile("file", "doc.pdf", "application/pdf", inflated);
|
new MockMultipartFile("file", "doc.pdf", MediaType.APPLICATION_PDF_VALUE, inflated);
|
||||||
PDFFile pdfFile = new PDFFile();
|
PDFFile pdfFile = new PDFFile();
|
||||||
pdfFile.setFileInput(multipart);
|
pdfFile.setFileInput(multipart);
|
||||||
try (PDDocument doc = factory.load(pdfFile)) {
|
try (PDDocument doc = factory.load(pdfFile)) {
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package stirling.software.common.service;
|
|||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
import static org.mockito.AdditionalAnswers.*;
|
import static org.mockito.AdditionalAnswers.*;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.Mockito.*;
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -15,6 +17,7 @@ import org.junit.jupiter.api.io.TempDir;
|
|||||||
import org.mockito.InjectMocks;
|
import org.mockito.InjectMocks;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.MockitoAnnotations;
|
import org.mockito.MockitoAnnotations;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.test.util.ReflectionTestUtils;
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
@@ -36,7 +39,7 @@ class FileStorageTest {
|
|||||||
// Create a mock MultipartFile
|
// Create a mock MultipartFile
|
||||||
mockFile = mock(MultipartFile.class);
|
mockFile = mock(MultipartFile.class);
|
||||||
when(mockFile.getOriginalFilename()).thenReturn("test.pdf");
|
when(mockFile.getOriginalFilename()).thenReturn("test.pdf");
|
||||||
when(mockFile.getContentType()).thenReturn("application/pdf");
|
when(mockFile.getContentType()).thenReturn(MediaType.APPLICATION_PDF_VALUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import org.junit.jupiter.api.Test;
|
|||||||
import org.mockito.InjectMocks;
|
import org.mockito.InjectMocks;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.MockitoAnnotations;
|
import org.mockito.MockitoAnnotations;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.test.util.ReflectionTestUtils;
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
|
||||||
import stirling.software.common.model.job.JobResult;
|
import stirling.software.common.model.job.JobResult;
|
||||||
@@ -77,7 +78,7 @@ class TaskManagerTest {
|
|||||||
taskManager.createTask(jobId);
|
taskManager.createTask(jobId);
|
||||||
String fileId = "file-id";
|
String fileId = "file-id";
|
||||||
String originalFileName = "test.pdf";
|
String originalFileName = "test.pdf";
|
||||||
String contentType = "application/pdf";
|
String contentType = MediaType.APPLICATION_PDF_VALUE;
|
||||||
long fileSize = 1024L;
|
long fileSize = 1024L;
|
||||||
|
|
||||||
// Mock the fileStorage.getFileSize() call
|
// Mock the fileStorage.getFileSize() call
|
||||||
@@ -185,7 +186,8 @@ class TaskManagerTest {
|
|||||||
// 2. Create completed successful job with file
|
// 2. Create completed successful job with file
|
||||||
String successFileJobId = "success-file-job";
|
String successFileJobId = "success-file-job";
|
||||||
taskManager.createTask(successFileJobId);
|
taskManager.createTask(successFileJobId);
|
||||||
taskManager.setFileResult(successFileJobId, "file-id", "test.pdf", "application/pdf");
|
taskManager.setFileResult(
|
||||||
|
successFileJobId, "file-id", "test.pdf", MediaType.APPLICATION_PDF_VALUE);
|
||||||
|
|
||||||
// 3. Create completed successful job without file
|
// 3. Create completed successful job without file
|
||||||
String successJobId = "success-job";
|
String successJobId = "success-job";
|
||||||
@@ -235,7 +237,7 @@ class TaskManagerTest {
|
|||||||
ResultFile.builder()
|
ResultFile.builder()
|
||||||
.fileId("file-id")
|
.fileId("file-id")
|
||||||
.fileName("test.pdf")
|
.fileName("test.pdf")
|
||||||
.contentType("application/pdf")
|
.contentType(MediaType.APPLICATION_PDF_VALUE)
|
||||||
.fileSize(1024L)
|
.fileSize(1024L)
|
||||||
.build();
|
.build();
|
||||||
ReflectionTestUtils.setField(oldJob, "resultFiles", java.util.List.of(resultFile));
|
ReflectionTestUtils.setField(oldJob, "resultFiles", java.util.List.of(resultFile));
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package stirling.software.common.util;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
public class ChecksumUtilsTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void computeChecksums_basic() throws Exception {
|
||||||
|
byte[] data = "hello".getBytes(StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
// MD5 (hex)
|
||||||
|
try (InputStream is = new ByteArrayInputStream(data)) {
|
||||||
|
assertEquals("5d41402abc4b2a76b9719d911017c592", ChecksumUtils.checksum(is, "MD5"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// MD5 (Base64)
|
||||||
|
try (InputStream is = new ByteArrayInputStream(data)) {
|
||||||
|
assertEquals("XUFAKrxLKna5cZ2REBfFkg==", ChecksumUtils.checksumBase64(is, "MD5"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// MD5 + CRC32 (hex map)
|
||||||
|
try (InputStream is = new ByteArrayInputStream(data)) {
|
||||||
|
Map<String, String> map = ChecksumUtils.checksums(is, "MD5", "CRC32");
|
||||||
|
assertEquals("5d41402abc4b2a76b9719d911017c592", map.get("MD5"));
|
||||||
|
assertEquals("3610a686", map.get("CRC32"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void crc32_base64_bigEndianBytes_forHello() throws Exception {
|
||||||
|
// CRC32("hello") = 0x3610A686 → bytes: 36 10 A6 86 → Base64: "NhCmhg=="
|
||||||
|
byte[] data = "hello".getBytes(StandardCharsets.UTF_8);
|
||||||
|
try (InputStream is = new ByteArrayInputStream(data)) {
|
||||||
|
assertEquals("NhCmhg==", ChecksumUtils.checksumBase64(is, "CRC32"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void crc32_unsignedFormatting_highBitSet() throws Exception {
|
||||||
|
// CRC32 of single zero byte (0x00) is 0xD202EF8D (>= 0x8000_0000)
|
||||||
|
byte[] data = new byte[] {0x00};
|
||||||
|
|
||||||
|
// Hex (unsigned, 8 chars, lowercase)
|
||||||
|
try (InputStream is = new ByteArrayInputStream(data)) {
|
||||||
|
assertEquals("d202ef8d", ChecksumUtils.checksum(is, "CRC32"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base64 of the 4-byte big-endian representation
|
||||||
|
try (InputStream is = new ByteArrayInputStream(data)) {
|
||||||
|
assertEquals("0gLvjQ==", ChecksumUtils.checksumBase64(is, "CRC32"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// matches(..) must be case-insensitive for hex
|
||||||
|
try (InputStream is = new ByteArrayInputStream("hello".getBytes(StandardCharsets.UTF_8))) {
|
||||||
|
assertTrue(ChecksumUtils.matches(is, "CRC32", "3610A686")); // uppercase expected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -586,7 +586,10 @@ class EmlToPdfTest {
|
|||||||
when(mockPdDocument.getNumberOfPages()).thenReturn(1);
|
when(mockPdDocument.getNumberOfPages()).thenReturn(1);
|
||||||
|
|
||||||
try (MockedStatic<FileToPdf> fileToPdf =
|
try (MockedStatic<FileToPdf> fileToPdf =
|
||||||
mockStatic(FileToPdf.class, org.mockito.Mockito.withSettings().lenient())) {
|
mockStatic(
|
||||||
|
FileToPdf.class,
|
||||||
|
org.mockito.Mockito.withSettings()
|
||||||
|
.defaultAnswer(org.mockito.Answers.RETURNS_DEFAULTS))) {
|
||||||
fileToPdf
|
fileToPdf
|
||||||
.when(
|
.when(
|
||||||
() ->
|
() ->
|
||||||
@@ -657,7 +660,10 @@ class EmlToPdfTest {
|
|||||||
when(mockPdDocument.getNumberOfPages()).thenReturn(1);
|
when(mockPdDocument.getNumberOfPages()).thenReturn(1);
|
||||||
|
|
||||||
try (MockedStatic<FileToPdf> fileToPdf =
|
try (MockedStatic<FileToPdf> fileToPdf =
|
||||||
mockStatic(FileToPdf.class, org.mockito.Mockito.withSettings().lenient())) {
|
mockStatic(
|
||||||
|
FileToPdf.class,
|
||||||
|
org.mockito.Mockito.withSettings()
|
||||||
|
.defaultAnswer(org.mockito.Answers.RETURNS_DEFAULTS))) {
|
||||||
fileToPdf
|
fileToPdf
|
||||||
.when(
|
.when(
|
||||||
() ->
|
() ->
|
||||||
@@ -724,7 +730,10 @@ class EmlToPdfTest {
|
|||||||
String errorMessage = "Conversion failed";
|
String errorMessage = "Conversion failed";
|
||||||
|
|
||||||
try (MockedStatic<FileToPdf> fileToPdf =
|
try (MockedStatic<FileToPdf> fileToPdf =
|
||||||
mockStatic(FileToPdf.class, org.mockito.Mockito.withSettings().lenient())) {
|
mockStatic(
|
||||||
|
FileToPdf.class,
|
||||||
|
org.mockito.Mockito.withSettings()
|
||||||
|
.defaultAnswer(org.mockito.Answers.RETURNS_DEFAULTS))) {
|
||||||
fileToPdf
|
fileToPdf
|
||||||
.when(
|
.when(
|
||||||
() ->
|
() ->
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
package stirling.software.common.util;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
import org.junit.jupiter.params.ParameterizedTest;
|
|
||||||
import org.junit.jupiter.params.provider.CsvSource;
|
|
||||||
|
|
||||||
import stirling.software.common.model.FileInfo;
|
|
||||||
|
|
||||||
public class FileInfoTest {
|
|
||||||
|
|
||||||
@ParameterizedTest(name = "{index}: fileSize={0}")
|
|
||||||
@CsvSource({
|
|
||||||
"0, '0 Bytes'",
|
|
||||||
"1023, '1023 Bytes'",
|
|
||||||
"1024, '1.00 KB'",
|
|
||||||
"1048575, '1024.00 KB'", // Do we really want this as result?
|
|
||||||
"1048576, '1.00 MB'",
|
|
||||||
"1073741823, '1024.00 MB'", // Do we really want this as result?
|
|
||||||
"1073741824, '1.00 GB'"
|
|
||||||
})
|
|
||||||
void testGetFormattedFileSize(long fileSize, String expectedFormattedSize) {
|
|
||||||
FileInfo fileInfo =
|
|
||||||
new FileInfo(
|
|
||||||
"example.txt",
|
|
||||||
"/path/to/example.txt",
|
|
||||||
LocalDateTime.now(),
|
|
||||||
fileSize,
|
|
||||||
LocalDateTime.now().minusDays(1));
|
|
||||||
|
|
||||||
assertEquals(expectedFormattedSize, fileInfo.getFormattedFileSize());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,15 @@
|
|||||||
package stirling.software.common.util;
|
package stirling.software.common.util;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
@@ -154,4 +161,223 @@ public class GeneralUtilsTest {
|
|||||||
List<Integer> result = GeneralUtils.parsePageList(new String[] {"1,3,7-8"}, 8, false);
|
List<Integer> result = GeneralUtils.parsePageList(new String[] {"1,3,7-8"}, 8, false);
|
||||||
assertEquals(List.of(0, 2, 6, 7), result, "Range should be parsed correctly.");
|
assertEquals(List.of(0, 2, 6, 7), result, "Range should be parsed correctly.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testRemoveExtension() {
|
||||||
|
// Test common cases (should use fast string operations)
|
||||||
|
assertEquals("document", GeneralUtils.removeExtension("document.pdf"));
|
||||||
|
assertEquals("image", GeneralUtils.removeExtension("image.jpg"));
|
||||||
|
assertEquals("file.backup", GeneralUtils.removeExtension("file.backup.zip"));
|
||||||
|
assertEquals("complex.file.name", GeneralUtils.removeExtension("complex.file.name.txt"));
|
||||||
|
|
||||||
|
// Test edge cases (should fall back to regex)
|
||||||
|
assertEquals("default", GeneralUtils.removeExtension(null));
|
||||||
|
assertEquals("noextension", GeneralUtils.removeExtension("noextension"));
|
||||||
|
assertEquals(
|
||||||
|
".hidden", GeneralUtils.removeExtension(".hidden")); // Hidden file, no extension
|
||||||
|
assertEquals("trailing.", GeneralUtils.removeExtension("trailing.")); // Trailing dot
|
||||||
|
assertEquals("", GeneralUtils.removeExtension(""));
|
||||||
|
assertEquals("a", GeneralUtils.removeExtension("a"));
|
||||||
|
|
||||||
|
// Test multiple dots
|
||||||
|
assertEquals("file.with.multiple", GeneralUtils.removeExtension("file.with.multiple.dots"));
|
||||||
|
assertEquals("path/to/file", GeneralUtils.removeExtension("path/to/file.ext"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testAppendSuffix() {
|
||||||
|
// Normal cases
|
||||||
|
assertEquals("document_processed", GeneralUtils.appendSuffix("document", "_processed"));
|
||||||
|
assertEquals("file.txt", GeneralUtils.appendSuffix("file", ".txt"));
|
||||||
|
|
||||||
|
// Null handling
|
||||||
|
assertEquals("default_suffix", GeneralUtils.appendSuffix(null, "_suffix"));
|
||||||
|
assertEquals("basename", GeneralUtils.appendSuffix("basename", null));
|
||||||
|
assertEquals("default", GeneralUtils.appendSuffix(null, null));
|
||||||
|
|
||||||
|
// Empty strings
|
||||||
|
assertEquals("_suffix", GeneralUtils.appendSuffix("", "_suffix"));
|
||||||
|
assertEquals("basename", GeneralUtils.appendSuffix("basename", ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testProcessFilenames() {
|
||||||
|
List<String> filenames = new ArrayList<>();
|
||||||
|
filenames.add("document.pdf");
|
||||||
|
filenames.add("image.jpg");
|
||||||
|
filenames.add("spreadsheet.xlsx");
|
||||||
|
filenames.add("presentation.pptx");
|
||||||
|
filenames.add(null); // Should handle null gracefully
|
||||||
|
filenames.add("noextension");
|
||||||
|
|
||||||
|
List<String> results = new ArrayList<>();
|
||||||
|
GeneralUtils.processFilenames(filenames, "_processed", results::add);
|
||||||
|
|
||||||
|
List<String> expected =
|
||||||
|
List.of(
|
||||||
|
"document_processed",
|
||||||
|
"image_processed",
|
||||||
|
"spreadsheet_processed",
|
||||||
|
"presentation_processed",
|
||||||
|
"default_processed",
|
||||||
|
"noextension_processed");
|
||||||
|
|
||||||
|
assertEquals(expected, results);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testProcessFilenamesNullHandling() {
|
||||||
|
List<String> results = new ArrayList<>();
|
||||||
|
|
||||||
|
// Null filenames list
|
||||||
|
GeneralUtils.processFilenames(null, "_suffix", results::add);
|
||||||
|
assertTrue(results.isEmpty(), "Should handle null filenames list");
|
||||||
|
|
||||||
|
// Null processor
|
||||||
|
List<String> filenames = List.of("test.txt");
|
||||||
|
GeneralUtils.processFilenames(filenames, "_suffix", null); // Should not throw
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testRemoveExtensionThreadSafety() throws InterruptedException {
|
||||||
|
final int threadCount = 50;
|
||||||
|
final int operationsPerThread = 100;
|
||||||
|
final String[] testFilenames = {
|
||||||
|
"document.pdf", "image.jpg", "data.csv", "presentation.pptx",
|
||||||
|
"archive.zip", "music.mp3", "video.mp4", "text.txt"
|
||||||
|
};
|
||||||
|
|
||||||
|
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
|
||||||
|
CountDownLatch latch = new CountDownLatch(threadCount);
|
||||||
|
AtomicInteger successCount = new AtomicInteger(0);
|
||||||
|
List<Exception> exceptions = Collections.synchronizedList(new ArrayList<>());
|
||||||
|
|
||||||
|
for (int i = 0; i < threadCount; i++) {
|
||||||
|
executor.submit(
|
||||||
|
() -> {
|
||||||
|
try {
|
||||||
|
for (int j = 0; j < operationsPerThread; j++) {
|
||||||
|
String filename = testFilenames[j % testFilenames.length];
|
||||||
|
String result = GeneralUtils.removeExtension(filename);
|
||||||
|
|
||||||
|
// Verify result is correct
|
||||||
|
assertFalse(
|
||||||
|
result.contains("."),
|
||||||
|
"Result should not contain extension: " + result);
|
||||||
|
assertTrue(
|
||||||
|
filename.startsWith(result),
|
||||||
|
"Original should start with result: "
|
||||||
|
+ filename
|
||||||
|
+ " -> "
|
||||||
|
+ result);
|
||||||
|
}
|
||||||
|
successCount.incrementAndGet();
|
||||||
|
} catch (Exception e) {
|
||||||
|
exceptions.add(e);
|
||||||
|
} finally {
|
||||||
|
latch.countDown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTrue(latch.await(10, TimeUnit.SECONDS), "All threads should complete");
|
||||||
|
|
||||||
|
if (!exceptions.isEmpty()) {
|
||||||
|
fail("Thread safety test failed with exceptions: " + exceptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(threadCount, successCount.get(), "All threads should succeed");
|
||||||
|
|
||||||
|
executor.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testBatchProcessingPerformance() {
|
||||||
|
List<String> filenames = new ArrayList<>();
|
||||||
|
for (int i = 0; i < 1000; i++) {
|
||||||
|
filenames.add("file" + i + ".pdf");
|
||||||
|
filenames.add("document" + i + ".docx");
|
||||||
|
filenames.add("image" + i + ".jpg");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> results = new ArrayList<>();
|
||||||
|
|
||||||
|
GeneralUtils.processFilenames(filenames, "_processed", results::add);
|
||||||
|
|
||||||
|
assertEquals(filenames.size(), results.size(), "Should process all filenames");
|
||||||
|
|
||||||
|
assertTrue(results.contains("file0_processed"), "Should contain processed filename");
|
||||||
|
assertTrue(results.contains("document500_processed"), "Should contain processed filename");
|
||||||
|
assertTrue(results.contains("image999_processed"), "Should contain processed filename");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testHybridStringRegexApproach() {
|
||||||
|
|
||||||
|
String[] edgeCases = {
|
||||||
|
"", // Empty string
|
||||||
|
".", // Just a dot
|
||||||
|
"..", // Two dots
|
||||||
|
"...", // Three dots
|
||||||
|
".hidden", // Hidden file
|
||||||
|
"file.", // Trailing dot
|
||||||
|
"a.b.c.d.e.f.g", // Many extensions
|
||||||
|
"no-extension-here", // No extension
|
||||||
|
"file..double.dot" // Double dots
|
||||||
|
};
|
||||||
|
|
||||||
|
for (String edgeCase : edgeCases) {
|
||||||
|
String result = GeneralUtils.removeExtension(edgeCase);
|
||||||
|
assertNotNull(result, "Result should not be null for: " + edgeCase);
|
||||||
|
|
||||||
|
// For specific edge cases, verify expected behavior
|
||||||
|
switch (edgeCase) {
|
||||||
|
case "" -> assertEquals("", result, "Empty string should remain empty");
|
||||||
|
case "." -> assertEquals(".", result, "Single dot should remain unchanged");
|
||||||
|
case ".." -> assertEquals("..", result, "Double dots should remain unchanged");
|
||||||
|
case "..." -> assertEquals("...", result, "Triple dots should remain unchanged");
|
||||||
|
case ".hidden" ->
|
||||||
|
assertEquals(".hidden", result, "Hidden file should remain unchanged");
|
||||||
|
case "file." ->
|
||||||
|
assertEquals("file.", result, "Trailing dot should remain unchanged");
|
||||||
|
case "no-extension-here" ->
|
||||||
|
assertEquals(
|
||||||
|
"no-extension-here",
|
||||||
|
result,
|
||||||
|
"No extension should remain unchanged");
|
||||||
|
case "a.b.c.d.e.f.g" ->
|
||||||
|
assertEquals(
|
||||||
|
"a.b.c.d.e.f",
|
||||||
|
result,
|
||||||
|
"Multiple extensions should remove last one");
|
||||||
|
case "file..double.dot" ->
|
||||||
|
assertEquals(
|
||||||
|
"file..double",
|
||||||
|
result,
|
||||||
|
"Double dot case should remove last extension");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetTitleFromFilename() {
|
||||||
|
// Test normal cases
|
||||||
|
assertEquals("document", GeneralUtils.getTitleFromFilename("document.pdf"));
|
||||||
|
assertEquals("presentation", GeneralUtils.getTitleFromFilename("presentation.pptx"));
|
||||||
|
assertEquals("file.backup", GeneralUtils.getTitleFromFilename("file.backup.zip"));
|
||||||
|
|
||||||
|
// Test null and empty handling
|
||||||
|
assertEquals("Untitled", GeneralUtils.getTitleFromFilename(null));
|
||||||
|
assertEquals("Untitled", GeneralUtils.getTitleFromFilename(""));
|
||||||
|
|
||||||
|
// Test edge cases
|
||||||
|
assertEquals(".hidden", GeneralUtils.getTitleFromFilename(".hidden"));
|
||||||
|
assertEquals("file.", GeneralUtils.getTitleFromFilename("file."));
|
||||||
|
assertEquals("noextension", GeneralUtils.getTitleFromFilename("noextension"));
|
||||||
|
|
||||||
|
// Test complex cases
|
||||||
|
assertEquals(
|
||||||
|
"complex.file.name", GeneralUtils.getTitleFromFilename("complex.file.name.txt"));
|
||||||
|
assertEquals("path/to/file", GeneralUtils.getTitleFromFilename("path/to/file.ext"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.anyList;
|
import static org.mockito.ArgumentMatchers.anyList;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
import static org.mockito.ArgumentMatchers.argThat;
|
import static org.mockito.ArgumentMatchers.argThat;
|
||||||
|
import static org.mockito.Mockito.lenient;
|
||||||
import static org.mockito.Mockito.mockStatic;
|
import static org.mockito.Mockito.mockStatic;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
@@ -25,6 +27,7 @@ import org.mockito.Mock;
|
|||||||
import org.mockito.MockedStatic;
|
import org.mockito.MockedStatic;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.mock.web.MockMultipartFile;
|
import org.springframework.mock.web.MockMultipartFile;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@@ -46,10 +49,21 @@ class PDFToFileTest {
|
|||||||
|
|
||||||
@Mock private ProcessExecutor mockProcessExecutor;
|
@Mock private ProcessExecutor mockProcessExecutor;
|
||||||
@Mock private ProcessExecutorResult mockExecutorResult;
|
@Mock private ProcessExecutorResult mockExecutorResult;
|
||||||
|
@Mock private TempFileManager mockTempFileManager;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() throws IOException {
|
||||||
pdfToFile = new PDFToFile();
|
// Mock the TempFileManager to return real temp files
|
||||||
|
lenient()
|
||||||
|
.when(mockTempFileManager.createTempFile(anyString()))
|
||||||
|
.thenAnswer(
|
||||||
|
invocation ->
|
||||||
|
Files.createTempFile("test", invocation.getArgument(0)).toFile());
|
||||||
|
lenient()
|
||||||
|
.when(mockTempFileManager.createTempDirectory())
|
||||||
|
.thenAnswer(invocation -> Files.createTempDirectory("test"));
|
||||||
|
|
||||||
|
pdfToFile = new PDFToFile(mockTempFileManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -57,7 +71,10 @@ class PDFToFileTest {
|
|||||||
// Prepare
|
// Prepare
|
||||||
MultipartFile nonPdfFile =
|
MultipartFile nonPdfFile =
|
||||||
new MockMultipartFile(
|
new MockMultipartFile(
|
||||||
"file", "test.txt", "text/plain", "This is not a PDF".getBytes());
|
"file",
|
||||||
|
"test.txt",
|
||||||
|
MediaType.TEXT_PLAIN_VALUE,
|
||||||
|
"This is not a PDF".getBytes());
|
||||||
|
|
||||||
// Execute
|
// Execute
|
||||||
ResponseEntity<byte[]> response = pdfToFile.processPdfToMarkdown(nonPdfFile);
|
ResponseEntity<byte[]> response = pdfToFile.processPdfToMarkdown(nonPdfFile);
|
||||||
@@ -71,7 +88,10 @@ class PDFToFileTest {
|
|||||||
// Prepare
|
// Prepare
|
||||||
MultipartFile nonPdfFile =
|
MultipartFile nonPdfFile =
|
||||||
new MockMultipartFile(
|
new MockMultipartFile(
|
||||||
"file", "test.txt", "text/plain", "This is not a PDF".getBytes());
|
"file",
|
||||||
|
"test.txt",
|
||||||
|
MediaType.TEXT_PLAIN_VALUE,
|
||||||
|
"This is not a PDF".getBytes());
|
||||||
|
|
||||||
// Execute
|
// Execute
|
||||||
ResponseEntity<byte[]> response = pdfToFile.processPdfToHtml(nonPdfFile);
|
ResponseEntity<byte[]> response = pdfToFile.processPdfToHtml(nonPdfFile);
|
||||||
@@ -86,7 +106,10 @@ class PDFToFileTest {
|
|||||||
// Prepare
|
// Prepare
|
||||||
MultipartFile nonPdfFile =
|
MultipartFile nonPdfFile =
|
||||||
new MockMultipartFile(
|
new MockMultipartFile(
|
||||||
"file", "test.txt", "text/plain", "This is not a PDF".getBytes());
|
"file",
|
||||||
|
"test.txt",
|
||||||
|
MediaType.TEXT_PLAIN_VALUE,
|
||||||
|
"This is not a PDF".getBytes());
|
||||||
|
|
||||||
// Execute
|
// Execute
|
||||||
ResponseEntity<byte[]> response =
|
ResponseEntity<byte[]> response =
|
||||||
@@ -102,7 +125,10 @@ class PDFToFileTest {
|
|||||||
// Prepare
|
// Prepare
|
||||||
MultipartFile pdfFile =
|
MultipartFile pdfFile =
|
||||||
new MockMultipartFile(
|
new MockMultipartFile(
|
||||||
"file", "test.pdf", "application/pdf", "Fake PDF content".getBytes());
|
"file",
|
||||||
|
"test.pdf",
|
||||||
|
MediaType.APPLICATION_PDF_VALUE,
|
||||||
|
"Fake PDF content".getBytes());
|
||||||
|
|
||||||
// Execute with invalid format
|
// Execute with invalid format
|
||||||
ResponseEntity<byte[]> response =
|
ResponseEntity<byte[]> response =
|
||||||
@@ -120,7 +146,10 @@ class PDFToFileTest {
|
|||||||
// Create a mock PDF file
|
// Create a mock PDF file
|
||||||
MultipartFile pdfFile =
|
MultipartFile pdfFile =
|
||||||
new MockMultipartFile(
|
new MockMultipartFile(
|
||||||
"file", "test.pdf", "application/pdf", "Fake PDF content".getBytes());
|
"file",
|
||||||
|
"test.pdf",
|
||||||
|
MediaType.APPLICATION_PDF_VALUE,
|
||||||
|
"Fake PDF content".getBytes());
|
||||||
|
|
||||||
// Create a mock HTML output file
|
// Create a mock HTML output file
|
||||||
Path htmlOutputFile = tempDir.resolve("test.html");
|
Path htmlOutputFile = tempDir.resolve("test.html");
|
||||||
@@ -168,7 +197,7 @@ class PDFToFileTest {
|
|||||||
new MockMultipartFile(
|
new MockMultipartFile(
|
||||||
"file",
|
"file",
|
||||||
"multipage.pdf",
|
"multipage.pdf",
|
||||||
"application/pdf",
|
MediaType.APPLICATION_PDF_VALUE,
|
||||||
"Fake PDF content".getBytes());
|
"Fake PDF content".getBytes());
|
||||||
|
|
||||||
// Setup ProcessExecutor mock
|
// Setup ProcessExecutor mock
|
||||||
@@ -245,7 +274,10 @@ class PDFToFileTest {
|
|||||||
// Create a mock PDF file
|
// Create a mock PDF file
|
||||||
MultipartFile pdfFile =
|
MultipartFile pdfFile =
|
||||||
new MockMultipartFile(
|
new MockMultipartFile(
|
||||||
"file", "test.pdf", "application/pdf", "Fake PDF content".getBytes());
|
"file",
|
||||||
|
"test.pdf",
|
||||||
|
MediaType.APPLICATION_PDF_VALUE,
|
||||||
|
"Fake PDF content".getBytes());
|
||||||
|
|
||||||
// Setup ProcessExecutor mock
|
// Setup ProcessExecutor mock
|
||||||
mockedStaticProcessExecutor
|
mockedStaticProcessExecutor
|
||||||
@@ -324,7 +356,7 @@ class PDFToFileTest {
|
|||||||
new MockMultipartFile(
|
new MockMultipartFile(
|
||||||
"file",
|
"file",
|
||||||
"document.pdf",
|
"document.pdf",
|
||||||
"application/pdf",
|
MediaType.APPLICATION_PDF_VALUE,
|
||||||
"Fake PDF content".getBytes());
|
"Fake PDF content".getBytes());
|
||||||
|
|
||||||
// Setup ProcessExecutor mock
|
// Setup ProcessExecutor mock
|
||||||
@@ -386,7 +418,7 @@ class PDFToFileTest {
|
|||||||
new MockMultipartFile(
|
new MockMultipartFile(
|
||||||
"file",
|
"file",
|
||||||
"document.pdf",
|
"document.pdf",
|
||||||
"application/pdf",
|
MediaType.APPLICATION_PDF_VALUE,
|
||||||
"Fake PDF content".getBytes());
|
"Fake PDF content".getBytes());
|
||||||
|
|
||||||
// Setup ProcessExecutor mock
|
// Setup ProcessExecutor mock
|
||||||
@@ -472,7 +504,7 @@ class PDFToFileTest {
|
|||||||
new MockMultipartFile(
|
new MockMultipartFile(
|
||||||
"file",
|
"file",
|
||||||
"document.pdf",
|
"document.pdf",
|
||||||
"application/pdf",
|
MediaType.APPLICATION_PDF_VALUE,
|
||||||
"Fake PDF content".getBytes());
|
"Fake PDF content".getBytes());
|
||||||
|
|
||||||
// Setup ProcessExecutor mock
|
// Setup ProcessExecutor mock
|
||||||
@@ -531,7 +563,10 @@ class PDFToFileTest {
|
|||||||
// Create a mock PDF file with no filename
|
// Create a mock PDF file with no filename
|
||||||
MultipartFile pdfFile =
|
MultipartFile pdfFile =
|
||||||
new MockMultipartFile(
|
new MockMultipartFile(
|
||||||
"file", "", "application/pdf", "Fake PDF content".getBytes());
|
"file",
|
||||||
|
"",
|
||||||
|
MediaType.APPLICATION_PDF_VALUE,
|
||||||
|
"Fake PDF content".getBytes());
|
||||||
|
|
||||||
// Setup ProcessExecutor mock
|
// Setup ProcessExecutor mock
|
||||||
mockedStaticProcessExecutor
|
mockedStaticProcessExecutor
|
||||||
|
|||||||
@@ -65,23 +65,22 @@ public class PdfUtilsTest {
|
|||||||
doc1.addPage(new PDPage());
|
doc1.addPage(new PDPage());
|
||||||
doc1.addPage(new PDPage());
|
doc1.addPage(new PDPage());
|
||||||
doc1.addPage(new PDPage());
|
doc1.addPage(new PDPage());
|
||||||
PdfUtils utils = new PdfUtils();
|
assertTrue(PdfUtils.pageCount(doc1, 2, "greater"));
|
||||||
assertTrue(utils.pageCount(doc1, 2, "greater"));
|
|
||||||
|
|
||||||
PDDocument doc2 = new PDDocument();
|
PDDocument doc2 = new PDDocument();
|
||||||
doc2.addPage(new PDPage());
|
doc2.addPage(new PDPage());
|
||||||
doc2.addPage(new PDPage());
|
doc2.addPage(new PDPage());
|
||||||
doc2.addPage(new PDPage());
|
doc2.addPage(new PDPage());
|
||||||
assertTrue(utils.pageCount(doc2, 3, "equal"));
|
assertTrue(PdfUtils.pageCount(doc2, 3, "equal"));
|
||||||
|
|
||||||
PDDocument doc3 = new PDDocument();
|
PDDocument doc3 = new PDDocument();
|
||||||
doc3.addPage(new PDPage());
|
doc3.addPage(new PDPage());
|
||||||
doc3.addPage(new PDPage());
|
doc3.addPage(new PDPage());
|
||||||
assertTrue(utils.pageCount(doc3, 5, "less"));
|
assertTrue(PdfUtils.pageCount(doc3, 5, "less"));
|
||||||
|
|
||||||
PDDocument doc4 = new PDDocument();
|
PDDocument doc4 = new PDDocument();
|
||||||
doc4.addPage(new PDPage());
|
doc4.addPage(new PDPage());
|
||||||
assertThrows(IllegalArgumentException.class, () -> utils.pageCount(doc4, 1, "bad"));
|
assertThrows(IllegalArgumentException.class, () -> PdfUtils.pageCount(doc4, 1, "bad"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -91,8 +90,7 @@ public class PdfUtilsTest {
|
|||||||
doc.addPage(page);
|
doc.addPage(page);
|
||||||
PDRectangle rect = page.getMediaBox();
|
PDRectangle rect = page.getMediaBox();
|
||||||
String expected = rect.getWidth() + "x" + rect.getHeight();
|
String expected = rect.getWidth() + "x" + rect.getHeight();
|
||||||
PdfUtils utils = new PdfUtils();
|
assertTrue(PdfUtils.pageSize(doc, expected));
|
||||||
assertTrue(utils.pageSize(doc, expected));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
package stirling.software.common.util;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
public class RegexPatternUtilsTest {
|
||||||
|
|
||||||
|
private RegexPatternUtils utils;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
utils = RegexPatternUtils.getInstance();
|
||||||
|
utils.clearCache(); // Start with clean cache for each test
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testPatternCaching() {
|
||||||
|
String regex = "test\\d+";
|
||||||
|
|
||||||
|
Pattern pattern1 = utils.getPattern(regex);
|
||||||
|
assertNotNull(pattern1);
|
||||||
|
assertTrue(utils.isCached(regex));
|
||||||
|
assertEquals(
|
||||||
|
1, utils.getCacheSize()); // Should have at least 1 pattern (plus precompiled ones
|
||||||
|
// are cleared)
|
||||||
|
|
||||||
|
Pattern pattern2 = utils.getPattern(regex);
|
||||||
|
assertSame(pattern1, pattern2); // Should be the exact same object
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testPatternWithFlags() {
|
||||||
|
String regex = "test";
|
||||||
|
int flags = Pattern.CASE_INSENSITIVE;
|
||||||
|
|
||||||
|
Pattern pattern1 = utils.getPattern(regex, flags);
|
||||||
|
Pattern pattern2 = utils.getPattern(regex); // No flags
|
||||||
|
|
||||||
|
assertNotSame(pattern1, pattern2); // Different flags = different cached patterns
|
||||||
|
assertTrue(utils.isCached(regex, flags));
|
||||||
|
assertTrue(utils.isCached(regex, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCacheEviction() {
|
||||||
|
String regex = "evict\\d+";
|
||||||
|
|
||||||
|
utils.getPattern(regex);
|
||||||
|
assertTrue(utils.isCached(regex));
|
||||||
|
|
||||||
|
boolean removed = utils.removeFromCache(regex);
|
||||||
|
assertTrue(removed);
|
||||||
|
assertFalse(utils.isCached(regex));
|
||||||
|
|
||||||
|
boolean removedAgain = utils.removeFromCache(regex);
|
||||||
|
assertFalse(removedAgain);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testNullRegexHandling() {
|
||||||
|
assertThrows(
|
||||||
|
IllegalArgumentException.class,
|
||||||
|
() -> {
|
||||||
|
utils.getPattern(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
assertThrows(
|
||||||
|
IllegalArgumentException.class,
|
||||||
|
() -> {
|
||||||
|
utils.getPattern(null, Pattern.CASE_INSENSITIVE);
|
||||||
|
});
|
||||||
|
|
||||||
|
assertFalse(utils.isCached(null));
|
||||||
|
assertFalse(utils.removeFromCache(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCommonPatterns() {
|
||||||
|
Pattern whitespace = utils.getWhitespacePattern();
|
||||||
|
assertTrue(whitespace.matcher(" \t ").matches());
|
||||||
|
|
||||||
|
Pattern trailing = utils.getTrailingSlashesPattern();
|
||||||
|
assertTrue(trailing.matcher("/path/to/dir///").find());
|
||||||
|
|
||||||
|
Pattern filename = utils.getSafeFilenamePattern();
|
||||||
|
assertTrue(filename.matcher("bad<file>name").find());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreateSearchPattern() {
|
||||||
|
String regex = "Hello";
|
||||||
|
|
||||||
|
Pattern caseSensitive = utils.createSearchPattern(regex, false);
|
||||||
|
Pattern caseInsensitive = utils.createSearchPattern(regex, true);
|
||||||
|
|
||||||
|
assertTrue(caseSensitive.matcher("Hello").matches());
|
||||||
|
assertFalse(caseSensitive.matcher("hello").matches());
|
||||||
|
|
||||||
|
assertTrue(caseInsensitive.matcher("Hello").matches());
|
||||||
|
assertTrue(caseInsensitive.matcher("hello").matches());
|
||||||
|
assertTrue(caseInsensitive.matcher("HELLO").matches());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSingletonBehavior() {
|
||||||
|
RegexPatternUtils instance1 = RegexPatternUtils.getInstance();
|
||||||
|
RegexPatternUtils instance2 = RegexPatternUtils.getInstance();
|
||||||
|
|
||||||
|
assertSame(instance1, instance2);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,7 +48,8 @@ public class WebResponseUtilsTest {
|
|||||||
try {
|
try {
|
||||||
byte[] fileContent = "Sample file content".getBytes();
|
byte[] fileContent = "Sample file content".getBytes();
|
||||||
MockMultipartFile file =
|
MockMultipartFile file =
|
||||||
new MockMultipartFile("file", "sample.txt", "text/plain", fileContent);
|
new MockMultipartFile(
|
||||||
|
"file", "sample.txt", MediaType.TEXT_PLAIN_VALUE, fileContent);
|
||||||
|
|
||||||
ResponseEntity<byte[]> responseEntity =
|
ResponseEntity<byte[]> responseEntity =
|
||||||
WebResponseUtils.multiPartFileToWebResponse(file);
|
WebResponseUtils.multiPartFileToWebResponse(file);
|
||||||
|
|||||||
+5
-1
@@ -8,6 +8,7 @@ import java.lang.reflect.Method;
|
|||||||
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.mock.web.MockMultipartFile;
|
import org.springframework.mock.web.MockMultipartFile;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
@@ -24,7 +25,10 @@ class CustomColorReplaceStrategyTest {
|
|||||||
// Create a mock file
|
// Create a mock file
|
||||||
mockFile =
|
mockFile =
|
||||||
new MockMultipartFile(
|
new MockMultipartFile(
|
||||||
"file", "test.pdf", "application/pdf", "test pdf content".getBytes());
|
"file",
|
||||||
|
"test.pdf",
|
||||||
|
MediaType.APPLICATION_PDF_VALUE,
|
||||||
|
"test pdf content".getBytes());
|
||||||
|
|
||||||
// Initialize strategy with custom colors
|
// Initialize strategy with custom colors
|
||||||
strategy =
|
strategy =
|
||||||
|
|||||||
+4
-1
@@ -24,6 +24,7 @@ import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceRGB;
|
|||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.core.io.InputStreamResource;
|
import org.springframework.core.io.InputStreamResource;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.mock.web.MockMultipartFile;
|
import org.springframework.mock.web.MockMultipartFile;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
@@ -38,7 +39,9 @@ class InvertFullColorStrategyTest {
|
|||||||
void setUp() throws Exception {
|
void setUp() throws Exception {
|
||||||
// Create a simple PDF document for testing
|
// Create a simple PDF document for testing
|
||||||
byte[] pdfBytes = createSimplePdfWithRectangle();
|
byte[] pdfBytes = createSimplePdfWithRectangle();
|
||||||
mockPdfFile = new MockMultipartFile("file", "test.pdf", "application/pdf", pdfBytes);
|
mockPdfFile =
|
||||||
|
new MockMultipartFile(
|
||||||
|
"file", "test.pdf", MediaType.APPLICATION_PDF_VALUE, pdfBytes);
|
||||||
|
|
||||||
// Create the strategy instance
|
// Create the strategy instance
|
||||||
strategy = new InvertFullColorStrategy(mockPdfFile, ReplaceAndInvert.FULL_INVERSION);
|
strategy = new InvertFullColorStrategy(mockPdfFile, ReplaceAndInvert.FULL_INVERSION);
|
||||||
|
|||||||
+14
-4
@@ -7,6 +7,7 @@ import java.io.IOException;
|
|||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.core.io.InputStreamResource;
|
import org.springframework.core.io.InputStreamResource;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.mock.web.MockMultipartFile;
|
import org.springframework.mock.web.MockMultipartFile;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
@@ -35,7 +36,10 @@ class ReplaceAndInvertColorStrategyTest {
|
|||||||
// Arrange
|
// Arrange
|
||||||
MultipartFile mockFile =
|
MultipartFile mockFile =
|
||||||
new MockMultipartFile(
|
new MockMultipartFile(
|
||||||
"file", "test.pdf", "application/pdf", "test content".getBytes());
|
"file",
|
||||||
|
"test.pdf",
|
||||||
|
MediaType.APPLICATION_PDF_VALUE,
|
||||||
|
"test content".getBytes());
|
||||||
ReplaceAndInvert replaceAndInvert = ReplaceAndInvert.CUSTOM_COLOR;
|
ReplaceAndInvert replaceAndInvert = ReplaceAndInvert.CUSTOM_COLOR;
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -56,7 +60,7 @@ class ReplaceAndInvertColorStrategyTest {
|
|||||||
// Arrange
|
// Arrange
|
||||||
byte[] content = "test pdf content".getBytes();
|
byte[] content = "test pdf content".getBytes();
|
||||||
MultipartFile mockFile =
|
MultipartFile mockFile =
|
||||||
new MockMultipartFile("file", "test.pdf", "application/pdf", content);
|
new MockMultipartFile("file", "test.pdf", MediaType.APPLICATION_PDF_VALUE, content);
|
||||||
ReplaceAndInvert replaceAndInvert = ReplaceAndInvert.CUSTOM_COLOR;
|
ReplaceAndInvert replaceAndInvert = ReplaceAndInvert.CUSTOM_COLOR;
|
||||||
|
|
||||||
ReplaceAndInvertColorStrategy strategy =
|
ReplaceAndInvertColorStrategy strategy =
|
||||||
@@ -74,10 +78,16 @@ class ReplaceAndInvertColorStrategyTest {
|
|||||||
// Arrange
|
// Arrange
|
||||||
MultipartFile mockFile1 =
|
MultipartFile mockFile1 =
|
||||||
new MockMultipartFile(
|
new MockMultipartFile(
|
||||||
"file1", "test1.pdf", "application/pdf", "content1".getBytes());
|
"file1",
|
||||||
|
"test1.pdf",
|
||||||
|
MediaType.APPLICATION_PDF_VALUE,
|
||||||
|
"content1".getBytes());
|
||||||
MultipartFile mockFile2 =
|
MultipartFile mockFile2 =
|
||||||
new MockMultipartFile(
|
new MockMultipartFile(
|
||||||
"file2", "test2.pdf", "application/pdf", "content2".getBytes());
|
"file2",
|
||||||
|
"test2.pdf",
|
||||||
|
MediaType.APPLICATION_PDF_VALUE,
|
||||||
|
"content2".getBytes());
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
ReplaceAndInvertColorStrategy strategy =
|
ReplaceAndInvertColorStrategy strategy =
|
||||||
|
|||||||
@@ -170,6 +170,10 @@ out/
|
|||||||
*.jks
|
*.jks
|
||||||
*.asc
|
*.asc
|
||||||
|
|
||||||
|
# test-cert
|
||||||
|
!**/test/resources/certs/test-cert.*
|
||||||
|
!**/test/resources/certs/test-key.*
|
||||||
|
|
||||||
# SSH Keys
|
# SSH Keys
|
||||||
*.pub
|
*.pub
|
||||||
*.priv
|
*.priv
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ dependencies {
|
|||||||
implementation 'commons-io:commons-io:2.20.0'
|
implementation 'commons-io:commons-io:2.20.0'
|
||||||
implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion"
|
implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion"
|
||||||
implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion"
|
implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion"
|
||||||
implementation 'io.micrometer:micrometer-core:1.15.2'
|
implementation 'io.micrometer:micrometer-core:1.15.4'
|
||||||
implementation 'com.google.zxing:core:3.5.3'
|
implementation 'com.google.zxing:core:3.5.3'
|
||||||
implementation "org.commonmark:commonmark:$commonmarkVersion" // https://mvnrepository.com/artifact/org.commonmark/commonmark
|
implementation "org.commonmark:commonmark:$commonmarkVersion" // https://mvnrepository.com/artifact/org.commonmark/commonmark
|
||||||
implementation "org.commonmark:commonmark-ext-gfm-tables:$commonmarkVersion"
|
implementation "org.commonmark:commonmark-ext-gfm-tables:$commonmarkVersion"
|
||||||
@@ -114,11 +114,6 @@ sourceSets {
|
|||||||
}
|
}
|
||||||
test {
|
test {
|
||||||
java {
|
java {
|
||||||
if (System.getenv('DOCKER_ENABLE_SECURITY') == 'false' || System.getenv('DISABLE_ADDITIONAL_FEATURES') == 'true'
|
|
||||||
|| (project.hasProperty('DISABLE_ADDITIONAL_FEATURES')
|
|
||||||
&& System.getProperty('DISABLE_ADDITIONAL_FEATURES') == 'true')) {
|
|
||||||
exclude 'stirling/software/proprietary/security/**'
|
|
||||||
}
|
|
||||||
if (System.getenv('STIRLING_PDF_DESKTOP_UI') == 'false') {
|
if (System.getenv('STIRLING_PDF_DESKTOP_UI') == 'false') {
|
||||||
exclude 'stirling/software/SPDF/UI/impl/**'
|
exclude 'stirling/software/SPDF/UI/impl/**'
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-27
@@ -42,11 +42,25 @@ import org.bouncycastle.operator.OperatorCreationException;
|
|||||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
||||||
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
|
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
public abstract class CreateSignatureBase implements SignatureInterface {
|
public abstract class CreateSignatureBase implements SignatureInterface {
|
||||||
private PrivateKey privateKey;
|
private PrivateKey privateKey;
|
||||||
private Certificate[] certificateChain;
|
@Getter private Certificate[] certificateChain;
|
||||||
private String tsaUrl;
|
@Setter private String tsaUrl;
|
||||||
private boolean externalSigning;
|
|
||||||
|
/**
|
||||||
|
* Specifies whether the external signing scenario should be used. If set to {@code true},
|
||||||
|
* external signing will be performed and {@link SignatureInterface} will be used for signing.
|
||||||
|
* If set to {@code false}, internal signing will be performed.
|
||||||
|
*
|
||||||
|
* <p>Default: {@code false}
|
||||||
|
*
|
||||||
|
* @param externalSigning {@code true} if external signing should be performed; {@code false}
|
||||||
|
* for internal signing
|
||||||
|
*/
|
||||||
|
@Setter @Getter private boolean externalSigning;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the signature creator with a keystore (pkcs12) and pin that should be used for the
|
* Initialize the signature creator with a keystore (pkcs12) and pin that should be used for the
|
||||||
@@ -97,18 +111,10 @@ public abstract class CreateSignatureBase implements SignatureInterface {
|
|||||||
this.privateKey = privateKey;
|
this.privateKey = privateKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Certificate[] getCertificateChain() {
|
|
||||||
return certificateChain;
|
|
||||||
}
|
|
||||||
|
|
||||||
public final void setCertificateChain(final Certificate[] certificateChain) {
|
public final void setCertificateChain(final Certificate[] certificateChain) {
|
||||||
this.certificateChain = certificateChain;
|
this.certificateChain = certificateChain;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setTsaUrl(String tsaUrl) {
|
|
||||||
this.tsaUrl = tsaUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SignatureInterface sample implementation.
|
* SignatureInterface sample implementation.
|
||||||
*
|
*
|
||||||
@@ -151,20 +157,4 @@ public abstract class CreateSignatureBase implements SignatureInterface {
|
|||||||
throw new IOException(e);
|
throw new IOException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isExternalSigning() {
|
|
||||||
return externalSigning;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set if external signing scenario should be used. If {@code false}, SignatureInterface would
|
|
||||||
* be used for signing.
|
|
||||||
*
|
|
||||||
* <p>Default: {@code false}
|
|
||||||
*
|
|
||||||
* @param externalSigning {@code true} if external signing should be performed
|
|
||||||
*/
|
|
||||||
public void setExternalSigning(boolean externalSigning) {
|
|
||||||
this.externalSigning = externalSigning;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-11
@@ -3,15 +3,22 @@ package stirling.software.SPDF.Factories;
|
|||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
import stirling.software.common.model.api.misc.HighContrastColorCombination;
|
import stirling.software.common.model.api.misc.HighContrastColorCombination;
|
||||||
import stirling.software.common.model.api.misc.ReplaceAndInvert;
|
import stirling.software.common.model.api.misc.ReplaceAndInvert;
|
||||||
|
import stirling.software.common.util.TempFileManager;
|
||||||
|
import stirling.software.common.util.misc.ColorSpaceConversionStrategy;
|
||||||
import stirling.software.common.util.misc.CustomColorReplaceStrategy;
|
import stirling.software.common.util.misc.CustomColorReplaceStrategy;
|
||||||
import stirling.software.common.util.misc.InvertFullColorStrategy;
|
import stirling.software.common.util.misc.InvertFullColorStrategy;
|
||||||
import stirling.software.common.util.misc.ReplaceAndInvertColorStrategy;
|
import stirling.software.common.util.misc.ReplaceAndInvertColorStrategy;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
public class ReplaceAndInvertColorFactory {
|
public class ReplaceAndInvertColorFactory {
|
||||||
|
|
||||||
|
private final TempFileManager tempFileManager;
|
||||||
|
|
||||||
public ReplaceAndInvertColorStrategy replaceAndInvert(
|
public ReplaceAndInvertColorStrategy replaceAndInvert(
|
||||||
MultipartFile file,
|
MultipartFile file,
|
||||||
ReplaceAndInvert replaceAndInvertOption,
|
ReplaceAndInvert replaceAndInvertOption,
|
||||||
@@ -19,21 +26,17 @@ public class ReplaceAndInvertColorFactory {
|
|||||||
String backGroundColor,
|
String backGroundColor,
|
||||||
String textColor) {
|
String textColor) {
|
||||||
|
|
||||||
if (replaceAndInvertOption == ReplaceAndInvert.CUSTOM_COLOR
|
return switch (replaceAndInvertOption) {
|
||||||
|| replaceAndInvertOption == ReplaceAndInvert.HIGH_CONTRAST_COLOR) {
|
case CUSTOM_COLOR, HIGH_CONTRAST_COLOR ->
|
||||||
|
new CustomColorReplaceStrategy(
|
||||||
return new CustomColorReplaceStrategy(
|
|
||||||
file,
|
file,
|
||||||
replaceAndInvertOption,
|
replaceAndInvertOption,
|
||||||
textColor,
|
textColor,
|
||||||
backGroundColor,
|
backGroundColor,
|
||||||
highContrastColorCombination);
|
highContrastColorCombination);
|
||||||
|
case FULL_INVERSION -> new InvertFullColorStrategy(file, replaceAndInvertOption);
|
||||||
} else if (replaceAndInvertOption == ReplaceAndInvert.FULL_INVERSION) {
|
case COLOR_SPACE_CONVERSION ->
|
||||||
|
new ColorSpaceConversionStrategy(file, replaceAndInvertOption, tempFileManager);
|
||||||
return new InvertFullColorStrategy(file, replaceAndInvertOption);
|
};
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,14 +46,11 @@ public class SPDFApplication {
|
|||||||
private final Environment env;
|
private final Environment env;
|
||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
// private final WebBrowser webBrowser; // Removed - desktop UI eliminated
|
|
||||||
|
|
||||||
public SPDFApplication(
|
public SPDFApplication(
|
||||||
AppConfig appConfig, Environment env, ApplicationProperties applicationProperties) {
|
AppConfig appConfig, Environment env, ApplicationProperties applicationProperties) {
|
||||||
this.appConfig = appConfig;
|
this.appConfig = appConfig;
|
||||||
this.env = env;
|
this.env = env;
|
||||||
this.applicationProperties = applicationProperties;
|
this.applicationProperties = applicationProperties;
|
||||||
// this.webBrowser = webBrowser; // Removed - desktop UI eliminated
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void main(String[] args) throws IOException, InterruptedException {
|
public static void main(String[] args) throws IOException, InterruptedException {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class AppUpdateService {
|
|||||||
@Scope("request")
|
@Scope("request")
|
||||||
public boolean shouldShow() {
|
public boolean shouldShow() {
|
||||||
boolean showUpdate = applicationProperties.getSystem().isShowUpdate();
|
boolean showUpdate = applicationProperties.getSystem().isShowUpdate();
|
||||||
boolean showAdminResult = (showAdmin != null) ? showAdmin.getShowUpdateOnlyAdmins() : true;
|
boolean showAdminResult = showAdmin == null || showAdmin.getShowUpdateOnlyAdmins();
|
||||||
return showUpdate && showAdminResult;
|
return showUpdate && showAdminResult;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ public class CleanUrlInterceptor implements HandlerInterceptor {
|
|||||||
"endpoints",
|
"endpoints",
|
||||||
"logout",
|
"logout",
|
||||||
"error",
|
"error",
|
||||||
|
"days",
|
||||||
|
"date",
|
||||||
"errorOAuth",
|
"errorOAuth",
|
||||||
"file",
|
"file",
|
||||||
"messageType",
|
"messageType",
|
||||||
@@ -64,7 +66,7 @@ public class CleanUrlInterceptor implements HandlerInterceptor {
|
|||||||
// Construct new query string
|
// Construct new query string
|
||||||
StringBuilder newQueryString = new StringBuilder();
|
StringBuilder newQueryString = new StringBuilder();
|
||||||
for (Map.Entry<String, String> entry : allowedParameters.entrySet()) {
|
for (Map.Entry<String, String> entry : allowedParameters.entrySet()) {
|
||||||
if (newQueryString.length() > 0) {
|
if (!newQueryString.isEmpty()) {
|
||||||
newQueryString.append("&");
|
newQueryString.append("&");
|
||||||
}
|
}
|
||||||
newQueryString.append(entry.getKey()).append("=").append(entry.getValue());
|
newQueryString.append(entry.getKey()).append("=").append(entry.getValue());
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.common.model.ApplicationProperties;
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
@@ -19,7 +20,7 @@ public class EndpointConfiguration {
|
|||||||
|
|
||||||
private static final String REMOVE_BLANKS = "remove-blanks";
|
private static final String REMOVE_BLANKS = "remove-blanks";
|
||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
private Map<String, Boolean> endpointStatuses = new ConcurrentHashMap<>();
|
@Getter private Map<String, Boolean> endpointStatuses = new ConcurrentHashMap<>();
|
||||||
private Map<String, Set<String>> endpointGroups = new ConcurrentHashMap<>();
|
private Map<String, Set<String>> endpointGroups = new ConcurrentHashMap<>();
|
||||||
private Set<String> disabledGroups = new HashSet<>();
|
private Set<String> disabledGroups = new HashSet<>();
|
||||||
private Map<String, Set<String>> endpointAlternatives = new ConcurrentHashMap<>();
|
private Map<String, Set<String>> endpointAlternatives = new ConcurrentHashMap<>();
|
||||||
@@ -46,10 +47,6 @@ public class EndpointConfiguration {
|
|||||||
endpointStatuses.put(endpoint, false);
|
endpointStatuses.put(endpoint, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Map<String, Boolean> getEndpointStatuses() {
|
|
||||||
return endpointStatuses;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isEndpointEnabled(String endpoint) {
|
public boolean isEndpointEnabled(String endpoint) {
|
||||||
String original = endpoint;
|
String original = endpoint;
|
||||||
if (endpoint.startsWith("/")) {
|
if (endpoint.startsWith("/")) {
|
||||||
@@ -391,6 +388,7 @@ public class EndpointConfiguration {
|
|||||||
addEndpointToGroup("Java", "pdf-to-markdown");
|
addEndpointToGroup("Java", "pdf-to-markdown");
|
||||||
addEndpointToGroup("Java", "add-attachments");
|
addEndpointToGroup("Java", "add-attachments");
|
||||||
addEndpointToGroup("Java", "compress-pdf");
|
addEndpointToGroup("Java", "compress-pdf");
|
||||||
|
addEndpointToGroup("rar", "pdf-to-cbr");
|
||||||
|
|
||||||
// Javascript
|
// Javascript
|
||||||
addEndpointToGroup("Javascript", "pdf-organizer");
|
addEndpointToGroup("Javascript", "pdf-organizer");
|
||||||
@@ -405,6 +403,8 @@ public class EndpointConfiguration {
|
|||||||
/* Ghostscript */
|
/* Ghostscript */
|
||||||
addEndpointToGroup("Ghostscript", "repair");
|
addEndpointToGroup("Ghostscript", "repair");
|
||||||
addEndpointToGroup("Ghostscript", "compress-pdf");
|
addEndpointToGroup("Ghostscript", "compress-pdf");
|
||||||
|
addEndpointToGroup("Ghostscript", "crop");
|
||||||
|
addEndpointToGroup("Ghostscript", "replace-invert-pdf");
|
||||||
|
|
||||||
/* tesseract */
|
/* tesseract */
|
||||||
addEndpointToGroup("tesseract", "ocr-pdf");
|
addEndpointToGroup("tesseract", "ocr-pdf");
|
||||||
@@ -488,7 +488,8 @@ public class EndpointConfiguration {
|
|||||||
|| "Java".equals(group)
|
|| "Java".equals(group)
|
||||||
|| "Javascript".equals(group)
|
|| "Javascript".equals(group)
|
||||||
|| "Weasyprint".equals(group)
|
|| "Weasyprint".equals(group)
|
||||||
|| "Pdftohtml".equals(group);
|
|| "Pdftohtml".equals(group)
|
||||||
|
|| "rar".equals(group);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isEndpointEnabledDirectly(String endpoint) {
|
private boolean isEndpointEnabledDirectly(String endpoint) {
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import java.util.Map;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.TreeSet;
|
import java.util.TreeSet;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.context.ApplicationContext;
|
import org.springframework.context.ApplicationContext;
|
||||||
import org.springframework.context.ApplicationListener;
|
import org.springframework.context.ApplicationListener;
|
||||||
import org.springframework.context.event.ContextRefreshedEvent;
|
import org.springframework.context.event.ContextRefreshedEvent;
|
||||||
@@ -18,11 +16,12 @@ import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
|
|||||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
public class EndpointInspector implements ApplicationListener<ContextRefreshedEvent> {
|
public class EndpointInspector implements ApplicationListener<ContextRefreshedEvent> {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(EndpointInspector.class);
|
|
||||||
|
|
||||||
private final ApplicationContext applicationContext;
|
private final ApplicationContext applicationContext;
|
||||||
private final Set<String> validGetEndpoints = new HashSet<>();
|
private final Set<String> validGetEndpoints = new HashSet<>();
|
||||||
@@ -71,13 +70,13 @@ public class EndpointInspector implements ApplicationListener<ContextRefreshedEv
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (validGetEndpoints.isEmpty()) {
|
if (validGetEndpoints.isEmpty()) {
|
||||||
logger.warn("No endpoints discovered. Adding common endpoints as fallback.");
|
log.warn("No endpoints discovered. Adding common endpoints as fallback.");
|
||||||
validGetEndpoints.add("/");
|
validGetEndpoints.add("/");
|
||||||
validGetEndpoints.add("/api/**");
|
validGetEndpoints.add("/api/**");
|
||||||
validGetEndpoints.add("/**");
|
validGetEndpoints.add("/**");
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("Error discovering endpoints", e);
|
log.error("Error discovering endpoints", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,10 +202,10 @@ public class EndpointInspector implements ApplicationListener<ContextRefreshedEv
|
|||||||
private void logAllEndpoints() {
|
private void logAllEndpoints() {
|
||||||
Set<String> sortedEndpoints = new TreeSet<>(validGetEndpoints);
|
Set<String> sortedEndpoints = new TreeSet<>(validGetEndpoints);
|
||||||
|
|
||||||
logger.info("=== BEGIN: All discovered GET endpoints ===");
|
log.info("=== BEGIN: All discovered GET endpoints ===");
|
||||||
for (String endpoint : sortedEndpoints) {
|
for (String endpoint : sortedEndpoints) {
|
||||||
logger.info("Endpoint: {}", endpoint);
|
log.info("Endpoint: {}", endpoint);
|
||||||
}
|
}
|
||||||
logger.info("=== END: All discovered GET endpoints ===");
|
log.info("=== END: All discovered GET endpoints ===");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import jakarta.annotation.PostConstruct;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.common.configuration.RuntimePathConfig;
|
import stirling.software.common.configuration.RuntimePathConfig;
|
||||||
|
import stirling.software.common.util.RegexPatternUtils;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -42,6 +43,7 @@ public class ExternalAppDepConfig {
|
|||||||
put(unoconvPath, List.of("Unoconvert"));
|
put(unoconvPath, List.of("Unoconvert"));
|
||||||
put("qpdf", List.of("qpdf"));
|
put("qpdf", List.of("qpdf"));
|
||||||
put("tesseract", List.of("tesseract"));
|
put("tesseract", List.of("tesseract"));
|
||||||
|
put("rar", List.of("rar")); // Required for real CBR output
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -73,7 +75,7 @@ public class ExternalAppDepConfig {
|
|||||||
// First replace common terms
|
// First replace common terms
|
||||||
String feature = endpoint.replace("-", " ").replace("pdf", "PDF").replace("img", "image");
|
String feature = endpoint.replace("-", " ").replace("pdf", "PDF").replace("img", "image");
|
||||||
// Split into words and capitalize each word
|
// Split into words and capitalize each word
|
||||||
return Arrays.stream(feature.split("\\s+"))
|
return Arrays.stream(RegexPatternUtils.getInstance().getWordSplitPattern().split(feature))
|
||||||
.map(word -> capitalizeWord(word))
|
.map(word -> capitalizeWord(word))
|
||||||
.collect(Collectors.joining(" "));
|
.collect(Collectors.joining(" "));
|
||||||
}
|
}
|
||||||
@@ -100,7 +102,7 @@ public class ExternalAppDepConfig {
|
|||||||
"Missing dependency: {} - Disabling group: {} (Affected features: {})",
|
"Missing dependency: {} - Disabling group: {} (Affected features: {})",
|
||||||
command,
|
command,
|
||||||
group,
|
group,
|
||||||
affectedFeatures != null && !affectedFeatures.isEmpty()
|
!affectedFeatures.isEmpty()
|
||||||
? String.join(", ", affectedFeatures)
|
? String.join(", ", affectedFeatures)
|
||||||
: "unknown");
|
: "unknown");
|
||||||
}
|
}
|
||||||
@@ -119,6 +121,7 @@ public class ExternalAppDepConfig {
|
|||||||
checkDependencyAndDisableGroup(weasyprintPath);
|
checkDependencyAndDisableGroup(weasyprintPath);
|
||||||
checkDependencyAndDisableGroup("pdftohtml");
|
checkDependencyAndDisableGroup("pdftohtml");
|
||||||
checkDependencyAndDisableGroup(unoconvPath);
|
checkDependencyAndDisableGroup(unoconvPath);
|
||||||
|
checkDependencyAndDisableGroup("rar");
|
||||||
// Special handling for Python/OpenCV dependencies
|
// Special handling for Python/OpenCV dependencies
|
||||||
boolean pythonAvailable = isCommandAvailable("python3") || isCommandAvailable("python");
|
boolean pythonAvailable = isCommandAvailable("python3") || isCommandAvailable("python");
|
||||||
if (!pythonAvailable) {
|
if (!pythonAvailable) {
|
||||||
|
|||||||
+11
-8
@@ -11,6 +11,7 @@ import org.apache.pdfbox.pdmodel.PDPageTree;
|
|||||||
import org.apache.pdfbox.pdmodel.encryption.PDEncryption;
|
import org.apache.pdfbox.pdmodel.encryption.PDEncryption;
|
||||||
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
|
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
|
||||||
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
|
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
@@ -29,7 +30,7 @@ public class AnalysisController {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@AutoJobPostMapping(value = "/page-count", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/page-count", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||||
@JsonDataResponse
|
@JsonDataResponse
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Get PDF page count",
|
summary = "Get PDF page count",
|
||||||
@@ -40,7 +41,7 @@ public class AnalysisController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@AutoJobPostMapping(value = "/basic-info", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/basic-info", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||||
@JsonDataResponse
|
@JsonDataResponse
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Get basic PDF information",
|
summary = "Get basic PDF information",
|
||||||
@@ -55,7 +56,9 @@ public class AnalysisController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@AutoJobPostMapping(value = "/document-properties", consumes = "multipart/form-data")
|
@AutoJobPostMapping(
|
||||||
|
value = "/document-properties",
|
||||||
|
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||||
@JsonDataResponse
|
@JsonDataResponse
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Get PDF document properties",
|
summary = "Get PDF document properties",
|
||||||
@@ -79,7 +82,7 @@ public class AnalysisController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@AutoJobPostMapping(value = "/page-dimensions", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/page-dimensions", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||||
@JsonDataResponse
|
@JsonDataResponse
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Get page dimensions for all pages",
|
summary = "Get page dimensions for all pages",
|
||||||
@@ -100,7 +103,7 @@ public class AnalysisController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@AutoJobPostMapping(value = "/form-fields", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/form-fields", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||||
@JsonDataResponse
|
@JsonDataResponse
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Get form field information",
|
summary = "Get form field information",
|
||||||
@@ -124,7 +127,7 @@ public class AnalysisController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@AutoJobPostMapping(value = "/annotation-info", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/annotation-info", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||||
@JsonDataResponse
|
@JsonDataResponse
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Get annotation information",
|
summary = "Get annotation information",
|
||||||
@@ -149,7 +152,7 @@ public class AnalysisController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@AutoJobPostMapping(value = "/font-info", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/font-info", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||||
@JsonDataResponse
|
@JsonDataResponse
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Get font information",
|
summary = "Get font information",
|
||||||
@@ -172,7 +175,7 @@ public class AnalysisController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@AutoJobPostMapping(value = "/security-info", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/security-info", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||||
@JsonDataResponse
|
@JsonDataResponse
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Get security information",
|
summary = "Get security information",
|
||||||
|
|||||||
+4
-1
@@ -13,6 +13,7 @@ import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
|||||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||||
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
||||||
import org.apache.pdfbox.util.Matrix;
|
import org.apache.pdfbox.util.Matrix;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
@@ -38,7 +39,9 @@ public class BookletImpositionController {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@AutoJobPostMapping(value = "/booklet-imposition", consumes = "multipart/form-data")
|
@AutoJobPostMapping(
|
||||||
|
value = "/booklet-imposition",
|
||||||
|
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Create a booklet with proper page imposition",
|
summary = "Create a booklet with proper page imposition",
|
||||||
description =
|
description =
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ package stirling.software.SPDF.controller.api;
|
|||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import org.apache.pdfbox.multipdf.LayerUtility;
|
import org.apache.pdfbox.multipdf.LayerUtility;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
@@ -10,27 +13,32 @@ import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
|||||||
import org.apache.pdfbox.pdmodel.PDPageContentStream.AppendMode;
|
import org.apache.pdfbox.pdmodel.PDPageContentStream.AppendMode;
|
||||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||||
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.config.swagger.StandardPdfResponse;
|
import stirling.software.SPDF.config.swagger.StandardPdfResponse;
|
||||||
import stirling.software.SPDF.model.api.general.CropPdfForm;
|
import stirling.software.SPDF.model.api.general.CropPdfForm;
|
||||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import stirling.software.common.annotations.api.GeneralApi;
|
import stirling.software.common.annotations.api.GeneralApi;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
|
import stirling.software.common.util.GeneralUtils;
|
||||||
|
import stirling.software.common.util.ProcessExecutor;
|
||||||
import stirling.software.common.util.WebResponseUtils;
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
|
|
||||||
@GeneralApi
|
@GeneralApi
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
public class CropController {
|
public class CropController {
|
||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@AutoJobPostMapping(value = "/crop", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/crop", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||||
@StandardPdfResponse
|
@StandardPdfResponse
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Crops a PDF document",
|
summary = "Crops a PDF document",
|
||||||
@@ -38,6 +46,15 @@ public class CropController {
|
|||||||
"This operation takes an input PDF file and crops it according to the given"
|
"This operation takes an input PDF file and crops it according to the given"
|
||||||
+ " coordinates. Input:PDF Output:PDF Type:SISO")
|
+ " coordinates. Input:PDF Output:PDF Type:SISO")
|
||||||
public ResponseEntity<byte[]> cropPdf(@ModelAttribute CropPdfForm request) throws IOException {
|
public ResponseEntity<byte[]> cropPdf(@ModelAttribute CropPdfForm request) throws IOException {
|
||||||
|
if (request.isRemoveDataOutsideCrop()) {
|
||||||
|
return cropWithGhostscript(request);
|
||||||
|
} else {
|
||||||
|
return cropWithPDFBox(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ResponseEntity<byte[]> cropWithPDFBox(@ModelAttribute CropPdfForm request)
|
||||||
|
throws IOException {
|
||||||
PDDocument sourceDocument = pdfDocumentFactory.load(request);
|
PDDocument sourceDocument = pdfDocumentFactory.load(request);
|
||||||
|
|
||||||
PDDocument newDocument =
|
PDDocument newDocument =
|
||||||
@@ -88,9 +105,64 @@ public class CropController {
|
|||||||
sourceDocument.close();
|
sourceDocument.close();
|
||||||
|
|
||||||
byte[] pdfContent = baos.toByteArray();
|
byte[] pdfContent = baos.toByteArray();
|
||||||
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
|
pdfContent,
|
||||||
|
GeneralUtils.generateFilename(
|
||||||
|
request.getFileInput().getOriginalFilename(), "_cropped.pdf"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ResponseEntity<byte[]> cropWithGhostscript(@ModelAttribute CropPdfForm request)
|
||||||
|
throws IOException {
|
||||||
|
PDDocument sourceDocument = pdfDocumentFactory.load(request);
|
||||||
|
|
||||||
|
for (int i = 0; i < sourceDocument.getNumberOfPages(); i++) {
|
||||||
|
PDPage page = sourceDocument.getPage(i);
|
||||||
|
PDRectangle cropBox =
|
||||||
|
new PDRectangle(
|
||||||
|
request.getX(),
|
||||||
|
request.getY(),
|
||||||
|
request.getWidth(),
|
||||||
|
request.getHeight());
|
||||||
|
page.setCropBox(cropBox);
|
||||||
|
}
|
||||||
|
|
||||||
|
Path tempInputFile = Files.createTempFile("crop_input", ".pdf");
|
||||||
|
Path tempOutputFile = Files.createTempFile("crop_output", ".pdf");
|
||||||
|
|
||||||
|
try {
|
||||||
|
sourceDocument.save(tempInputFile.toFile());
|
||||||
|
sourceDocument.close();
|
||||||
|
|
||||||
|
ProcessExecutor processExecutor =
|
||||||
|
ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT);
|
||||||
|
List<String> command =
|
||||||
|
List.of(
|
||||||
|
"gs",
|
||||||
|
"-sDEVICE=pdfwrite",
|
||||||
|
"-dUseCropBox",
|
||||||
|
"-o",
|
||||||
|
tempOutputFile.toString(),
|
||||||
|
tempInputFile.toString());
|
||||||
|
|
||||||
|
processExecutor.runCommandWithOutputHandling(command);
|
||||||
|
|
||||||
|
byte[] pdfContent = Files.readAllBytes(tempOutputFile);
|
||||||
|
|
||||||
return WebResponseUtils.bytesToWebResponse(
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
pdfContent,
|
pdfContent,
|
||||||
request.getFileInput().getOriginalFilename().replaceFirst("[.][^.]+$", "")
|
request.getFileInput().getOriginalFilename().replaceFirst("[.][^.]+$", "")
|
||||||
+ "_cropped.pdf");
|
+ "_cropped.pdf");
|
||||||
|
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new IOException("Ghostscript processing was interrupted", e);
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
Files.deleteIfExists(tempInputFile);
|
||||||
|
Files.deleteIfExists(tempOutputFile);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.debug("Failed to delete temporary files", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-28
@@ -13,6 +13,7 @@ import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlin
|
|||||||
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineNode;
|
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineNode;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.ResponseBody;
|
import org.springframework.web.bind.annotation.ResponseBody;
|
||||||
@@ -23,7 +24,9 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.config.swagger.JsonDataResponse;
|
import stirling.software.SPDF.config.swagger.JsonDataResponse;
|
||||||
@@ -32,6 +35,7 @@ import stirling.software.SPDF.model.api.EditTableOfContentsRequest;
|
|||||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import stirling.software.common.annotations.api.GeneralApi;
|
import stirling.software.common.annotations.api.GeneralApi;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
|
import stirling.software.common.util.GeneralUtils;
|
||||||
import stirling.software.common.util.WebResponseUtils;
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
|
|
||||||
@GeneralApi
|
@GeneralApi
|
||||||
@@ -42,7 +46,9 @@ public class EditTableOfContentsController {
|
|||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
@AutoJobPostMapping(value = "/extract-bookmarks", consumes = "multipart/form-data")
|
@AutoJobPostMapping(
|
||||||
|
value = "/extract-bookmarks",
|
||||||
|
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||||
@JsonDataResponse
|
@JsonDataResponse
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Extract PDF Bookmarks",
|
summary = "Extract PDF Bookmarks",
|
||||||
@@ -151,7 +157,9 @@ public class EditTableOfContentsController {
|
|||||||
return bookmark;
|
return bookmark;
|
||||||
}
|
}
|
||||||
|
|
||||||
@AutoJobPostMapping(value = "/edit-table-of-contents", consumes = "multipart/form-data")
|
@AutoJobPostMapping(
|
||||||
|
value = "/edit-table-of-contents",
|
||||||
|
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||||
@StandardPdfResponse
|
@StandardPdfResponse
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Edit Table of Contents",
|
summary = "Edit Table of Contents",
|
||||||
@@ -180,9 +188,10 @@ public class EditTableOfContentsController {
|
|||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
document.save(baos);
|
document.save(baos);
|
||||||
|
|
||||||
String filename = file.getOriginalFilename().replaceFirst("[.][^.]+$", "");
|
|
||||||
return WebResponseUtils.bytesToWebResponse(
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
baos.toByteArray(), filename + "_with_toc.pdf", MediaType.APPLICATION_PDF);
|
baos.toByteArray(),
|
||||||
|
GeneralUtils.generateFilename(file.getOriginalFilename(), "_with_toc.pdf"),
|
||||||
|
MediaType.APPLICATION_PDF);
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
if (document != null) {
|
if (document != null) {
|
||||||
@@ -234,33 +243,11 @@ public class EditTableOfContentsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Inner class to represent bookmarks in JSON
|
// Inner class to represent bookmarks in JSON
|
||||||
|
@Setter
|
||||||
|
@Getter
|
||||||
public static class BookmarkItem {
|
public static class BookmarkItem {
|
||||||
private String title;
|
private String title;
|
||||||
private int pageNumber;
|
private int pageNumber;
|
||||||
private List<BookmarkItem> children = new ArrayList<>();
|
private List<BookmarkItem> children = new ArrayList<>();
|
||||||
|
|
||||||
public String getTitle() {
|
|
||||||
return title;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTitle(String title) {
|
|
||||||
this.title = title;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getPageNumber() {
|
|
||||||
return pageNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPageNumber(int pageNumber) {
|
|
||||||
this.pageNumber = pageNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<BookmarkItem> getChildren() {
|
|
||||||
return children;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setChildren(List<BookmarkItem> children) {
|
|
||||||
this.children = children;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+159
-106
@@ -1,12 +1,8 @@
|
|||||||
package stirling.software.SPDF.controller.api;
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.io.InputStream;
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.nio.file.attribute.BasicFileAttributes;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
@@ -15,17 +11,23 @@ import java.util.List;
|
|||||||
import org.apache.pdfbox.multipdf.PDFMergerUtility;
|
import org.apache.pdfbox.multipdf.PDFMergerUtility;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
|
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocumentInformation;
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
|
import org.apache.pdfbox.pdmodel.common.PDMetadata;
|
||||||
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDDocumentOutline;
|
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDDocumentOutline;
|
||||||
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineItem;
|
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineItem;
|
||||||
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
|
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
|
||||||
import org.apache.pdfbox.pdmodel.interactive.form.PDField;
|
import org.apache.pdfbox.pdmodel.interactive.form.PDField;
|
||||||
import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField;
|
import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.apache.xmpbox.XMPMetadata;
|
||||||
|
import org.apache.xmpbox.schema.XMPBasicSchema;
|
||||||
|
import org.apache.xmpbox.xml.DomXmpParser;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
|
||||||
@@ -40,6 +42,8 @@ import stirling.software.common.service.CustomPDFDocumentFactory;
|
|||||||
import stirling.software.common.util.ExceptionUtils;
|
import stirling.software.common.util.ExceptionUtils;
|
||||||
import stirling.software.common.util.GeneralUtils;
|
import stirling.software.common.util.GeneralUtils;
|
||||||
import stirling.software.common.util.PdfErrorUtils;
|
import stirling.software.common.util.PdfErrorUtils;
|
||||||
|
import stirling.software.common.util.TempFile;
|
||||||
|
import stirling.software.common.util.TempFileManager;
|
||||||
import stirling.software.common.util.WebResponseUtils;
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
|
|
||||||
@GeneralApi
|
@GeneralApi
|
||||||
@@ -48,6 +52,7 @@ import stirling.software.common.util.WebResponseUtils;
|
|||||||
public class MergeController {
|
public class MergeController {
|
||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
private final TempFileManager tempFileManager;
|
||||||
|
|
||||||
// Merges a list of PDDocument objects into a single PDDocument
|
// Merges a list of PDDocument objects into a single PDDocument
|
||||||
public PDDocument mergeDocuments(List<PDDocument> documents) throws IOException {
|
public PDDocument mergeDocuments(List<PDDocument> documents) throws IOException {
|
||||||
@@ -60,58 +65,78 @@ public class MergeController {
|
|||||||
return mergedDoc;
|
return mergedDoc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Re-order files to match the explicit order provided by the front-end.
|
||||||
|
// fileOrder is newline-delimited original filenames in the desired order.
|
||||||
|
private static MultipartFile[] reorderFilesByProvidedOrder(
|
||||||
|
MultipartFile[] files, String fileOrder) {
|
||||||
|
String[] desired = fileOrder.split("\n", -1);
|
||||||
|
List<MultipartFile> remaining = new ArrayList<>(Arrays.asList(files));
|
||||||
|
List<MultipartFile> ordered = new ArrayList<>(files.length);
|
||||||
|
|
||||||
|
for (String name : desired) {
|
||||||
|
if (name == null || name.isEmpty()) continue;
|
||||||
|
int idx = indexOfByOriginalFilename(remaining, name);
|
||||||
|
if (idx >= 0) {
|
||||||
|
ordered.add(remaining.remove(idx));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append any files not explicitly listed, preserving their relative order
|
||||||
|
ordered.addAll(remaining);
|
||||||
|
return ordered.toArray(new MultipartFile[0]);
|
||||||
|
}
|
||||||
|
|
||||||
// Returns a comparator for sorting MultipartFile arrays based on the given sort type
|
// Returns a comparator for sorting MultipartFile arrays based on the given sort type
|
||||||
private Comparator<MultipartFile> getSortComparator(String sortType) {
|
private Comparator<MultipartFile> getSortComparator(String sortType) {
|
||||||
switch (sortType) {
|
return switch (sortType) {
|
||||||
case "byFileName":
|
case "byFileName" ->
|
||||||
return Comparator.comparing(MultipartFile::getOriginalFilename);
|
Comparator.comparing(
|
||||||
case "byDateModified":
|
(MultipartFile mf) -> {
|
||||||
return (file1, file2) -> {
|
String name = mf.getOriginalFilename();
|
||||||
try {
|
return name == null ? "" : name;
|
||||||
BasicFileAttributes attr1 =
|
},
|
||||||
Files.readAttributes(
|
String.CASE_INSENSITIVE_ORDER);
|
||||||
Paths.get(file1.getOriginalFilename()),
|
case "byDateModified" ->
|
||||||
BasicFileAttributes.class);
|
(file1, file2) -> {
|
||||||
BasicFileAttributes attr2 =
|
long t1 = getPdfDateTimeSafe(file1);
|
||||||
Files.readAttributes(
|
long t2 = getPdfDateTimeSafe(file2);
|
||||||
Paths.get(file2.getOriginalFilename()),
|
return Long.compare(t2, t1);
|
||||||
BasicFileAttributes.class);
|
|
||||||
return attr1.lastModifiedTime().compareTo(attr2.lastModifiedTime());
|
|
||||||
} catch (IOException e) {
|
|
||||||
return 0; // If there's an error, treat them as equal
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
case "byDateCreated":
|
case "byDateCreated" ->
|
||||||
return (file1, file2) -> {
|
(file1, file2) -> {
|
||||||
try {
|
long t1 = getPdfDateTimeSafe(file1);
|
||||||
BasicFileAttributes attr1 =
|
long t2 = getPdfDateTimeSafe(file2);
|
||||||
Files.readAttributes(
|
return Long.compare(t2, t1);
|
||||||
Paths.get(file1.getOriginalFilename()),
|
|
||||||
BasicFileAttributes.class);
|
|
||||||
BasicFileAttributes attr2 =
|
|
||||||
Files.readAttributes(
|
|
||||||
Paths.get(file2.getOriginalFilename()),
|
|
||||||
BasicFileAttributes.class);
|
|
||||||
return attr1.creationTime().compareTo(attr2.creationTime());
|
|
||||||
} catch (IOException e) {
|
|
||||||
return 0; // If there's an error, treat them as equal
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
case "byPDFTitle":
|
case "byPDFTitle" ->
|
||||||
return (file1, file2) -> {
|
(file1, file2) -> {
|
||||||
try (PDDocument doc1 = pdfDocumentFactory.load(file1);
|
try (PDDocument doc1 = pdfDocumentFactory.load(file1);
|
||||||
PDDocument doc2 = pdfDocumentFactory.load(file2)) {
|
PDDocument doc2 = pdfDocumentFactory.load(file2)) {
|
||||||
String title1 = doc1.getDocumentInformation().getTitle();
|
String title1 =
|
||||||
String title2 = doc2.getDocumentInformation().getTitle();
|
doc1.getDocumentInformation() != null
|
||||||
return title1.compareTo(title2);
|
? doc1.getDocumentInformation().getTitle()
|
||||||
|
: null;
|
||||||
|
String title2 =
|
||||||
|
doc2.getDocumentInformation() != null
|
||||||
|
? doc2.getDocumentInformation().getTitle()
|
||||||
|
: null;
|
||||||
|
if (title1 == null && title2 == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (title1 == null) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (title2 == null) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return title1.compareToIgnoreCase(title2);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
case "orderProvided":
|
case "orderProvided" -> (file1, file2) -> 0; // Default is the order provided
|
||||||
default:
|
default -> (file1, file2) -> 0; // Default is the order provided
|
||||||
return (file1, file2) -> 0; // Default is the order provided
|
};
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse client file IDs from JSON string
|
// Parse client file IDs from JSON string
|
||||||
@@ -152,10 +177,7 @@ public class MergeController {
|
|||||||
for (MultipartFile file : files) {
|
for (MultipartFile file : files) {
|
||||||
// Get the filename without extension to use as bookmark title
|
// Get the filename without extension to use as bookmark title
|
||||||
String filename = file.getOriginalFilename();
|
String filename = file.getOriginalFilename();
|
||||||
String title = filename;
|
String title = GeneralUtils.removeExtension(filename);
|
||||||
if (title != null && title.contains(".")) {
|
|
||||||
title = title.substring(0, title.lastIndexOf('.'));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create an outline item for this file
|
// Create an outline item for this file
|
||||||
PDOutlineItem item = new PDOutlineItem();
|
PDOutlineItem item = new PDOutlineItem();
|
||||||
@@ -180,7 +202,56 @@ public class MergeController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/merge-pdfs")
|
private long getPdfDateTimeSafe(MultipartFile file) {
|
||||||
|
try {
|
||||||
|
try (PDDocument doc = pdfDocumentFactory.load(file)) {
|
||||||
|
PDDocumentInformation info = doc.getDocumentInformation();
|
||||||
|
if (info != null) {
|
||||||
|
if (info.getModificationDate() != null) {
|
||||||
|
return info.getModificationDate().getTimeInMillis();
|
||||||
|
}
|
||||||
|
if (info.getCreationDate() != null) {
|
||||||
|
return info.getCreationDate().getTimeInMillis();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to XMP metadata if Info dates are missing
|
||||||
|
PDMetadata metadata = doc.getDocumentCatalog().getMetadata();
|
||||||
|
if (metadata != null) {
|
||||||
|
try (InputStream is = metadata.createInputStream()) {
|
||||||
|
DomXmpParser parser = new DomXmpParser();
|
||||||
|
XMPMetadata xmp = parser.parse(is);
|
||||||
|
XMPBasicSchema basic = xmp.getXMPBasicSchema();
|
||||||
|
if (basic != null) {
|
||||||
|
if (basic.getModifyDate() != null) {
|
||||||
|
return basic.getModifyDate().getTimeInMillis();
|
||||||
|
}
|
||||||
|
if (basic.getCreateDate() != null) {
|
||||||
|
return basic.getCreateDate().getTimeInMillis();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug(
|
||||||
|
"Unable to read XMP metadata dates from uploaded file: {}",
|
||||||
|
e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.debug("Unable to read PDF dates from uploaded file: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int indexOfByOriginalFilename(List<MultipartFile> list, String name) {
|
||||||
|
for (int i = 0; i < list.size(); i++) {
|
||||||
|
MultipartFile f = list.get(i);
|
||||||
|
if (name.equals(f.getOriginalFilename())) return i;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/merge-pdfs")
|
||||||
@StandardPdfResponse
|
@StandardPdfResponse
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Merge multiple PDF files into one",
|
summary = "Merge multiple PDF files into one",
|
||||||
@@ -188,21 +259,34 @@ public class MergeController {
|
|||||||
"This endpoint merges multiple PDF files into a single PDF file. The merged"
|
"This endpoint merges multiple PDF files into a single PDF file. The merged"
|
||||||
+ " file will contain all pages from the input files in the order they were"
|
+ " file will contain all pages from the input files in the order they were"
|
||||||
+ " provided. Input:PDF Output:PDF Type:MISO")
|
+ " provided. Input:PDF Output:PDF Type:MISO")
|
||||||
public ResponseEntity<byte[]> mergePdfs(@ModelAttribute MergePdfsRequest request)
|
public ResponseEntity<StreamingResponseBody> mergePdfs(
|
||||||
|
@ModelAttribute MergePdfsRequest request,
|
||||||
|
@RequestParam(value = "fileOrder", required = false) String fileOrder)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
List<File> filesToDelete = new ArrayList<>(); // List of temporary files to delete
|
List<File> filesToDelete = new ArrayList<>(); // List of temporary files to delete
|
||||||
File mergedTempFile = null;
|
TempFile outputTempFile = null;
|
||||||
PDDocument mergedDocument = null;
|
|
||||||
|
|
||||||
boolean removeCertSign = Boolean.TRUE.equals(request.getRemoveCertSign());
|
boolean removeCertSign = Boolean.TRUE.equals(request.getRemoveCertSign());
|
||||||
boolean generateToc = request.isGenerateToc();
|
boolean generateToc = request.isGenerateToc();
|
||||||
|
|
||||||
try {
|
|
||||||
MultipartFile[] files = request.getFileInput();
|
MultipartFile[] files = request.getFileInput();
|
||||||
|
if (files == null) {
|
||||||
|
files = new MultipartFile[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// If front-end provided explicit visible order, honor it and override backend sorting
|
||||||
|
if (fileOrder != null && !fileOrder.isBlank()) {
|
||||||
|
files = reorderFilesByProvidedOrder(files, fileOrder);
|
||||||
|
} else {
|
||||||
Arrays.sort(
|
Arrays.sort(
|
||||||
files,
|
files,
|
||||||
getSortComparator(
|
getSortComparator(
|
||||||
request.getSortType())); // Sort files based on the given sort type
|
request.getSortType())); // Sort files based on requested sort type
|
||||||
|
}
|
||||||
|
|
||||||
|
ResponseEntity<StreamingResponseBody> response;
|
||||||
|
|
||||||
|
try (TempFile mt = new TempFile(tempFileManager, ".pdf")) {
|
||||||
|
|
||||||
PDFMergerUtility mergerUtility = new PDFMergerUtility();
|
PDFMergerUtility mergerUtility = new PDFMergerUtility();
|
||||||
long totalSize = 0;
|
long totalSize = 0;
|
||||||
@@ -211,7 +295,7 @@ public class MergeController {
|
|||||||
MultipartFile multipartFile = files[index];
|
MultipartFile multipartFile = files[index];
|
||||||
totalSize += multipartFile.getSize();
|
totalSize += multipartFile.getSize();
|
||||||
File tempFile =
|
File tempFile =
|
||||||
GeneralUtils.convertMultipartFileToFile(
|
tempFileManager.convertMultipartFileToFile(
|
||||||
multipartFile); // Convert MultipartFile to File
|
multipartFile); // Convert MultipartFile to File
|
||||||
filesToDelete.add(tempFile); // Add temp file to the list for later deletion
|
filesToDelete.add(tempFile); // Add temp file to the list for later deletion
|
||||||
|
|
||||||
@@ -226,30 +310,7 @@ public class MergeController {
|
|||||||
mergerUtility.addSource(tempFile); // Add source file to the merger utility
|
mergerUtility.addSource(tempFile); // Add source file to the merger utility
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!invalidIndexes.isEmpty()) {
|
mergerUtility.setDestinationFileName(mt.getFile().getAbsolutePath());
|
||||||
// Parse client file IDs (always present from frontend)
|
|
||||||
String[] clientIds = parseClientFileIds(request.getClientFileIds());
|
|
||||||
|
|
||||||
// Map invalid indexes to client IDs
|
|
||||||
List<String> errorFileIds = new ArrayList<>();
|
|
||||||
for (Integer index : invalidIndexes) {
|
|
||||||
if (index < clientIds.length) {
|
|
||||||
errorFileIds.add(clientIds[index]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String payload =
|
|
||||||
String.format(
|
|
||||||
"{\"errorFileIds\":%s,\"message\":\"Some of the selected files can't be merged\"}",
|
|
||||||
errorFileIds.toString());
|
|
||||||
|
|
||||||
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY)
|
|
||||||
.header("Content-Type", MediaType.APPLICATION_JSON_VALUE)
|
|
||||||
.body(payload.getBytes(StandardCharsets.UTF_8));
|
|
||||||
}
|
|
||||||
|
|
||||||
mergedTempFile = Files.createTempFile("merged-", ".pdf").toFile();
|
|
||||||
mergerUtility.setDestinationFileName(mergedTempFile.getAbsolutePath());
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
mergerUtility.mergeDocuments(
|
mergerUtility.mergeDocuments(
|
||||||
@@ -263,9 +324,8 @@ public class MergeController {
|
|||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load the merged PDF document
|
// Load the merged PDF document and operate on it inside try-with-resources
|
||||||
mergedDocument = pdfDocumentFactory.load(mergedTempFile);
|
try (PDDocument mergedDocument = pdfDocumentFactory.load(mt.getFile())) {
|
||||||
|
|
||||||
// Remove signatures if removeCertSign is true
|
// Remove signatures if removeCertSign is true
|
||||||
if (removeCertSign) {
|
if (removeCertSign) {
|
||||||
PDDocumentCatalog catalog = mergedDocument.getDocumentCatalog();
|
PDDocumentCatalog catalog = mergedDocument.getDocumentCatalog();
|
||||||
@@ -273,7 +333,7 @@ public class MergeController {
|
|||||||
if (acroForm != null) {
|
if (acroForm != null) {
|
||||||
List<PDField> fieldsToRemove =
|
List<PDField> fieldsToRemove =
|
||||||
acroForm.getFields().stream()
|
acroForm.getFields().stream()
|
||||||
.filter(field -> field instanceof PDSignatureField)
|
.filter(PDSignatureField.class::isInstance)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
if (!fieldsToRemove.isEmpty()) {
|
if (!fieldsToRemove.isEmpty()) {
|
||||||
@@ -289,16 +349,10 @@ public class MergeController {
|
|||||||
addTableOfContents(mergedDocument, files);
|
addTableOfContents(mergedDocument, files);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save the modified document to a new ByteArrayOutputStream
|
// Save the modified document to a temporary file
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
outputTempFile = new TempFile(tempFileManager, ".pdf");
|
||||||
mergedDocument.save(baos);
|
mergedDocument.save(outputTempFile.getFile());
|
||||||
|
}
|
||||||
String mergedFileName =
|
|
||||||
files[0].getOriginalFilename().replaceFirst("[.][^.]+$", "")
|
|
||||||
+ "_merged_unsigned.pdf";
|
|
||||||
return WebResponseUtils.baosToWebResponse(
|
|
||||||
baos, mergedFileName); // Return the modified PDF
|
|
||||||
|
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
if (ex instanceof IOException && PdfErrorUtils.isCorruptedPdfError((IOException) ex)) {
|
if (ex instanceof IOException && PdfErrorUtils.isCorruptedPdfError((IOException) ex)) {
|
||||||
log.warn("Corrupted PDF detected in merge pdf process: {}", ex.getMessage());
|
log.warn("Corrupted PDF detected in merge pdf process: {}", ex.getMessage());
|
||||||
@@ -307,17 +361,16 @@ public class MergeController {
|
|||||||
}
|
}
|
||||||
throw ex;
|
throw ex;
|
||||||
} finally {
|
} finally {
|
||||||
if (mergedDocument != null) {
|
|
||||||
mergedDocument.close(); // Close the merged document
|
|
||||||
}
|
|
||||||
for (File file : filesToDelete) {
|
for (File file : filesToDelete) {
|
||||||
if (file != null) {
|
tempFileManager.deleteTempFile(file); // Delete temporary files
|
||||||
Files.deleteIfExists(file.toPath()); // Delete temporary files
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (mergedTempFile != null) {
|
|
||||||
Files.deleteIfExists(mergedTempFile.toPath());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String firstFilename = files.length > 0 ? files[0].getOriginalFilename() : null;
|
||||||
|
String mergedFileName =
|
||||||
|
GeneralUtils.generateFilename(firstFilename, "_merged_unsigned.pdf");
|
||||||
|
|
||||||
|
response = WebResponseUtils.pdfFileToWebResponse(outputTempFile, mergedFileName);
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user