Add application
This commit is contained in:
parent
dba06df091
commit
62bb338898
16 changed files with 1977 additions and 0 deletions
14
Pipfile
Normal file
14
Pipfile
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
[[source]]
|
||||||
|
url = "https://pypi.org/simple"
|
||||||
|
verify_ssl = true
|
||||||
|
name = "pypi"
|
||||||
|
|
||||||
|
[packages]
|
||||||
|
pillow = "*"
|
||||||
|
aiohttp = "*"
|
||||||
|
aiohttp-jinja2 = "*"
|
||||||
|
|
||||||
|
[dev-packages]
|
||||||
|
|
||||||
|
[requires]
|
||||||
|
python_version = "3.11"
|
588
Pipfile.lock
generated
Normal file
588
Pipfile.lock
generated
Normal file
|
@ -0,0 +1,588 @@
|
||||||
|
{
|
||||||
|
"_meta": {
|
||||||
|
"hash": {
|
||||||
|
"sha256": "0c8d4376bace43c538f4e1c619ac0a13ecf25da4135c47c3ac11a2c46c3102c9"
|
||||||
|
},
|
||||||
|
"pipfile-spec": 6,
|
||||||
|
"requires": {
|
||||||
|
"python_version": "3.11"
|
||||||
|
},
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"name": "pypi",
|
||||||
|
"url": "https://pypi.org/simple",
|
||||||
|
"verify_ssl": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"aiohttp": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:00ad4b6f185ec67f3e6562e8a1d2b69660be43070bd0ef6fcec5211154c7df67",
|
||||||
|
"sha256:0175d745d9e85c40dcc51c8f88c74bfbaef9e7afeeeb9d03c37977270303064c",
|
||||||
|
"sha256:01d4c0c874aa4ddfb8098e85d10b5e875a70adc63db91f1ae65a4b04d3344cda",
|
||||||
|
"sha256:043d2299f6dfdc92f0ac5e995dfc56668e1587cea7f9aa9d8a78a1b6554e5755",
|
||||||
|
"sha256:0c413c633d0512df4dc7fd2373ec06cc6a815b7b6d6c2f208ada7e9e93a5061d",
|
||||||
|
"sha256:0d21c684808288a98914e5aaf2a7c6a3179d4df11d249799c32d1808e79503b5",
|
||||||
|
"sha256:0e584a10f204a617d71d359fe383406305a4b595b333721fa50b867b4a0a1548",
|
||||||
|
"sha256:1274477e4c71ce8cfe6c1ec2f806d57c015ebf84d83373676036e256bc55d690",
|
||||||
|
"sha256:13bf85afc99ce6f9ee3567b04501f18f9f8dbbb2ea11ed1a2e079670403a7c84",
|
||||||
|
"sha256:153c2549f6c004d2754cc60603d4668899c9895b8a89397444a9c4efa282aaf4",
|
||||||
|
"sha256:1f7372f7341fcc16f57b2caded43e81ddd18df53320b6f9f042acad41f8e049a",
|
||||||
|
"sha256:23fb25a9f0a1ca1f24c0a371523546366bb642397c94ab45ad3aedf2941cec6a",
|
||||||
|
"sha256:28c543e54710d6158fc6f439296c7865b29e0b616629767e685a7185fab4a6b9",
|
||||||
|
"sha256:2a482e6da906d5e6e653be079b29bc173a48e381600161c9932d89dfae5942ef",
|
||||||
|
"sha256:2ad5c3c4590bb3cc28b4382f031f3783f25ec223557124c68754a2231d989e2b",
|
||||||
|
"sha256:2ce2ac5708501afc4847221a521f7e4b245abf5178cf5ddae9d5b3856ddb2f3a",
|
||||||
|
"sha256:2cf57fb50be5f52bda004b8893e63b48530ed9f0d6c96c84620dc92fe3cd9b9d",
|
||||||
|
"sha256:2e1b1e51b0774408f091d268648e3d57f7260c1682e7d3a63cb00d22d71bb945",
|
||||||
|
"sha256:2e2e9839e14dd5308ee773c97115f1e0a1cb1d75cbeeee9f33824fa5144c7634",
|
||||||
|
"sha256:2e460be6978fc24e3df83193dc0cc4de46c9909ed92dd47d349a452ef49325b7",
|
||||||
|
"sha256:312fcfbacc7880a8da0ae8b6abc6cc7d752e9caa0051a53d217a650b25e9a691",
|
||||||
|
"sha256:33279701c04351a2914e1100b62b2a7fdb9a25995c4a104259f9a5ead7ed4802",
|
||||||
|
"sha256:33776e945d89b29251b33a7e7d006ce86447b2cfd66db5e5ded4e5cd0340585c",
|
||||||
|
"sha256:34dd0c107799dcbbf7d48b53be761a013c0adf5571bf50c4ecad5643fe9cfcd0",
|
||||||
|
"sha256:3562b06567c06439d8b447037bb655ef69786c590b1de86c7ab81efe1c9c15d8",
|
||||||
|
"sha256:368a42363c4d70ab52c2c6420a57f190ed3dfaca6a1b19afda8165ee16416a82",
|
||||||
|
"sha256:4149d34c32f9638f38f544b3977a4c24052042affa895352d3636fa8bffd030a",
|
||||||
|
"sha256:461908b2578955045efde733719d62f2b649c404189a09a632d245b445c9c975",
|
||||||
|
"sha256:4a01951fabc4ce26ab791da5f3f24dca6d9a6f24121746eb19756416ff2d881b",
|
||||||
|
"sha256:4e874cbf8caf8959d2adf572a78bba17cb0e9d7e51bb83d86a3697b686a0ab4d",
|
||||||
|
"sha256:4f21e83f355643c345177a5d1d8079f9f28b5133bcd154193b799d380331d5d3",
|
||||||
|
"sha256:5443910d662db951b2e58eb70b0fbe6b6e2ae613477129a5805d0b66c54b6cb7",
|
||||||
|
"sha256:5798a9aad1879f626589f3df0f8b79b3608a92e9beab10e5fda02c8a2c60db2e",
|
||||||
|
"sha256:5d20003b635fc6ae3f96d7260281dfaf1894fc3aa24d1888a9b2628e97c241e5",
|
||||||
|
"sha256:5db3a5b833764280ed7618393832e0853e40f3d3e9aa128ac0ba0f8278d08649",
|
||||||
|
"sha256:5ed1c46fb119f1b59304b5ec89f834f07124cd23ae5b74288e364477641060ff",
|
||||||
|
"sha256:62360cb771707cb70a6fd114b9871d20d7dd2163a0feafe43fd115cfe4fe845e",
|
||||||
|
"sha256:6809a00deaf3810e38c628e9a33271892f815b853605a936e2e9e5129762356c",
|
||||||
|
"sha256:68c5a82c8779bdfc6367c967a4a1b2aa52cd3595388bf5961a62158ee8a59e22",
|
||||||
|
"sha256:6e4a280e4b975a2e7745573e3fc9c9ba0d1194a3738ce1cbaa80626cc9b4f4df",
|
||||||
|
"sha256:6e6783bcc45f397fdebc118d772103d751b54cddf5b60fbcc958382d7dd64f3e",
|
||||||
|
"sha256:72a860c215e26192379f57cae5ab12b168b75db8271f111019509a1196dfc780",
|
||||||
|
"sha256:7607ec3ce4993464368505888af5beb446845a014bc676d349efec0e05085905",
|
||||||
|
"sha256:773dd01706d4db536335fcfae6ea2440a70ceb03dd3e7378f3e815b03c97ab51",
|
||||||
|
"sha256:78d847e4cde6ecc19125ccbc9bfac4a7ab37c234dd88fbb3c5c524e8e14da543",
|
||||||
|
"sha256:7dde0009408969a43b04c16cbbe252c4f5ef4574ac226bc8815cd7342d2028b6",
|
||||||
|
"sha256:80bd372b8d0715c66c974cf57fe363621a02f359f1ec81cba97366948c7fc873",
|
||||||
|
"sha256:841cd8233cbd2111a0ef0a522ce016357c5e3aff8a8ce92bcfa14cef890d698f",
|
||||||
|
"sha256:84de26ddf621d7ac4c975dbea4c945860e08cccde492269db4e1538a6a6f3c35",
|
||||||
|
"sha256:84f8ae3e09a34f35c18fa57f015cc394bd1389bce02503fb30c394d04ee6b938",
|
||||||
|
"sha256:8af740fc2711ad85f1a5c034a435782fbd5b5f8314c9a3ef071424a8158d7f6b",
|
||||||
|
"sha256:8b929b9bd7cd7c3939f8bcfffa92fae7480bd1aa425279d51a89327d600c704d",
|
||||||
|
"sha256:910bec0c49637d213f5d9877105d26e0c4a4de2f8b1b29405ff37e9fc0ad52b8",
|
||||||
|
"sha256:96943e5dcc37a6529d18766597c491798b7eb7a61d48878611298afc1fca946c",
|
||||||
|
"sha256:a0215ce6041d501f3155dc219712bc41252d0ab76474615b9700d63d4d9292af",
|
||||||
|
"sha256:a3cf433f127efa43fee6b90ea4c6edf6c4a17109d1d037d1a52abec84d8f2e42",
|
||||||
|
"sha256:a6ce61195c6a19c785df04e71a4537e29eaa2c50fe745b732aa937c0c77169f3",
|
||||||
|
"sha256:a7a75ef35f2df54ad55dbf4b73fe1da96f370e51b10c91f08b19603c64004acc",
|
||||||
|
"sha256:a94159871304770da4dd371f4291b20cac04e8c94f11bdea1c3478e557fbe0d8",
|
||||||
|
"sha256:aa1990247f02a54185dc0dff92a6904521172a22664c863a03ff64c42f9b5410",
|
||||||
|
"sha256:ab88bafedc57dd0aab55fa728ea10c1911f7e4d8b43e1d838a1739f33712921c",
|
||||||
|
"sha256:ad093e823df03bb3fd37e7dec9d4670c34f9e24aeace76808fc20a507cace825",
|
||||||
|
"sha256:ae871a964e1987a943d83d6709d20ec6103ca1eaf52f7e0d36ee1b5bebb8b9b9",
|
||||||
|
"sha256:b0ba0d15164eae3d878260d4c4df859bbdc6466e9e6689c344a13334f988bb53",
|
||||||
|
"sha256:b5411d82cddd212644cf9360879eb5080f0d5f7d809d03262c50dad02f01421a",
|
||||||
|
"sha256:b9552ec52cc147dbf1944ac7ac98af7602e51ea2dcd076ed194ca3c0d1c7d0bc",
|
||||||
|
"sha256:bfb9162dcf01f615462b995a516ba03e769de0789de1cadc0f916265c257e5d8",
|
||||||
|
"sha256:c0a9034379a37ae42dea7ac1e048352d96286626251862e448933c0f59cbd79c",
|
||||||
|
"sha256:c1161b345c0a444ebcf46bf0a740ba5dcf50612fd3d0528883fdc0eff578006a",
|
||||||
|
"sha256:c11f5b099adafb18e65c2c997d57108b5bbeaa9eeee64a84302c0978b1ec948b",
|
||||||
|
"sha256:c44e65da1de4403d0576473e2344828ef9c4c6244d65cf4b75549bb46d40b8dd",
|
||||||
|
"sha256:c48c5c0271149cfe467c0ff8eb941279fd6e3f65c9a388c984e0e6cf57538e14",
|
||||||
|
"sha256:c7a815258e5895d8900aec4454f38dca9aed71085f227537208057853f9d13f2",
|
||||||
|
"sha256:cae533195e8122584ec87531d6df000ad07737eaa3c81209e85c928854d2195c",
|
||||||
|
"sha256:cc14be025665dba6202b6a71cfcdb53210cc498e50068bc088076624471f8bb9",
|
||||||
|
"sha256:cd56db019015b6acfaaf92e1ac40eb8434847d9bf88b4be4efe5bfd260aee692",
|
||||||
|
"sha256:d827176898a2b0b09694fbd1088c7a31836d1a505c243811c87ae53a3f6273c1",
|
||||||
|
"sha256:df72ac063b97837a80d80dec8d54c241af059cc9bb42c4de68bd5b61ceb37caa",
|
||||||
|
"sha256:e5980a746d547a6ba173fd5ee85ce9077e72d118758db05d229044b469d9029a",
|
||||||
|
"sha256:e5d47ae48db0b2dcf70bc8a3bc72b3de86e2a590fc299fdbbb15af320d2659de",
|
||||||
|
"sha256:e91d635961bec2d8f19dfeb41a539eb94bd073f075ca6dae6c8dc0ee89ad6f91",
|
||||||
|
"sha256:ea353162f249c8097ea63c2169dd1aa55de1e8fecbe63412a9bc50816e87b761",
|
||||||
|
"sha256:eaeed7abfb5d64c539e2db173f63631455f1196c37d9d8d873fc316470dfbacd",
|
||||||
|
"sha256:eca4bf3734c541dc4f374ad6010a68ff6c6748f00451707f39857f429ca36ced",
|
||||||
|
"sha256:f83a552443a526ea38d064588613aca983d0ee0038801bc93c0c916428310c28",
|
||||||
|
"sha256:fb1558def481d84f03b45888473fc5a1f35747b5f334ef4e7a571bc0dfcb11f8",
|
||||||
|
"sha256:fd1ed388ea7fbed22c4968dd64bab0198de60750a25fe8c0c9d4bef5abe13824"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==3.8.5"
|
||||||
|
},
|
||||||
|
"aiohttp-jinja2": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:45cf00b80ab4dcc19515df13a929826eeb9698e76a3bcfd99112418751f5a061",
|
||||||
|
"sha256:8d149b2a57d91f794b33a394ea5bc66b567f38c74a5a6a9477afc2450f105c01"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==1.5.1"
|
||||||
|
},
|
||||||
|
"aiosignal": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc",
|
||||||
|
"sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==1.3.1"
|
||||||
|
},
|
||||||
|
"async-timeout": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f",
|
||||||
|
"sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==4.0.3"
|
||||||
|
},
|
||||||
|
"attrs": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04",
|
||||||
|
"sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==23.1.0"
|
||||||
|
},
|
||||||
|
"charset-normalizer": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96",
|
||||||
|
"sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c",
|
||||||
|
"sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710",
|
||||||
|
"sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706",
|
||||||
|
"sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020",
|
||||||
|
"sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252",
|
||||||
|
"sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad",
|
||||||
|
"sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329",
|
||||||
|
"sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a",
|
||||||
|
"sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f",
|
||||||
|
"sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6",
|
||||||
|
"sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4",
|
||||||
|
"sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a",
|
||||||
|
"sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46",
|
||||||
|
"sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2",
|
||||||
|
"sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23",
|
||||||
|
"sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace",
|
||||||
|
"sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd",
|
||||||
|
"sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982",
|
||||||
|
"sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10",
|
||||||
|
"sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2",
|
||||||
|
"sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea",
|
||||||
|
"sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09",
|
||||||
|
"sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5",
|
||||||
|
"sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149",
|
||||||
|
"sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489",
|
||||||
|
"sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9",
|
||||||
|
"sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80",
|
||||||
|
"sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592",
|
||||||
|
"sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3",
|
||||||
|
"sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6",
|
||||||
|
"sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed",
|
||||||
|
"sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c",
|
||||||
|
"sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200",
|
||||||
|
"sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a",
|
||||||
|
"sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e",
|
||||||
|
"sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d",
|
||||||
|
"sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6",
|
||||||
|
"sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623",
|
||||||
|
"sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669",
|
||||||
|
"sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3",
|
||||||
|
"sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa",
|
||||||
|
"sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9",
|
||||||
|
"sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2",
|
||||||
|
"sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f",
|
||||||
|
"sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1",
|
||||||
|
"sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4",
|
||||||
|
"sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a",
|
||||||
|
"sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8",
|
||||||
|
"sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3",
|
||||||
|
"sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029",
|
||||||
|
"sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f",
|
||||||
|
"sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959",
|
||||||
|
"sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22",
|
||||||
|
"sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7",
|
||||||
|
"sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952",
|
||||||
|
"sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346",
|
||||||
|
"sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e",
|
||||||
|
"sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d",
|
||||||
|
"sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299",
|
||||||
|
"sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd",
|
||||||
|
"sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a",
|
||||||
|
"sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3",
|
||||||
|
"sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037",
|
||||||
|
"sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94",
|
||||||
|
"sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c",
|
||||||
|
"sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858",
|
||||||
|
"sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a",
|
||||||
|
"sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449",
|
||||||
|
"sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c",
|
||||||
|
"sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918",
|
||||||
|
"sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1",
|
||||||
|
"sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c",
|
||||||
|
"sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac",
|
||||||
|
"sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa"
|
||||||
|
],
|
||||||
|
"markers": "python_full_version >= '3.7.0'",
|
||||||
|
"version": "==3.2.0"
|
||||||
|
},
|
||||||
|
"frozenlist": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:007df07a6e3eb3e33e9a1fe6a9db7af152bbd8a185f9aaa6ece10a3529e3e1c6",
|
||||||
|
"sha256:008eb8b31b3ea6896da16c38c1b136cb9fec9e249e77f6211d479db79a4eaf01",
|
||||||
|
"sha256:09163bdf0b2907454042edb19f887c6d33806adc71fbd54afc14908bfdc22251",
|
||||||
|
"sha256:0c7c1b47859ee2cac3846fde1c1dc0f15da6cec5a0e5c72d101e0f83dcb67ff9",
|
||||||
|
"sha256:0e5c8764c7829343d919cc2dfc587a8db01c4f70a4ebbc49abde5d4b158b007b",
|
||||||
|
"sha256:10ff5faaa22786315ef57097a279b833ecab1a0bfb07d604c9cbb1c4cdc2ed87",
|
||||||
|
"sha256:17ae5cd0f333f94f2e03aaf140bb762c64783935cc764ff9c82dff626089bebf",
|
||||||
|
"sha256:19488c57c12d4e8095a922f328df3f179c820c212940a498623ed39160bc3c2f",
|
||||||
|
"sha256:1a0848b52815006ea6596c395f87449f693dc419061cc21e970f139d466dc0a0",
|
||||||
|
"sha256:1e78fb68cf9c1a6aa4a9a12e960a5c9dfbdb89b3695197aa7064705662515de2",
|
||||||
|
"sha256:261b9f5d17cac914531331ff1b1d452125bf5daa05faf73b71d935485b0c510b",
|
||||||
|
"sha256:2b8bcf994563466db019fab287ff390fffbfdb4f905fc77bc1c1d604b1c689cc",
|
||||||
|
"sha256:38461d02d66de17455072c9ba981d35f1d2a73024bee7790ac2f9e361ef1cd0c",
|
||||||
|
"sha256:490132667476f6781b4c9458298b0c1cddf237488abd228b0b3650e5ecba7467",
|
||||||
|
"sha256:491e014f5c43656da08958808588cc6c016847b4360e327a62cb308c791bd2d9",
|
||||||
|
"sha256:515e1abc578dd3b275d6a5114030b1330ba044ffba03f94091842852f806f1c1",
|
||||||
|
"sha256:556de4430ce324c836789fa4560ca62d1591d2538b8ceb0b4f68fb7b2384a27a",
|
||||||
|
"sha256:5833593c25ac59ede40ed4de6d67eb42928cca97f26feea219f21d0ed0959b79",
|
||||||
|
"sha256:6221d84d463fb110bdd7619b69cb43878a11d51cbb9394ae3105d082d5199167",
|
||||||
|
"sha256:6918d49b1f90821e93069682c06ffde41829c346c66b721e65a5c62b4bab0300",
|
||||||
|
"sha256:6c38721585f285203e4b4132a352eb3daa19121a035f3182e08e437cface44bf",
|
||||||
|
"sha256:71932b597f9895f011f47f17d6428252fc728ba2ae6024e13c3398a087c2cdea",
|
||||||
|
"sha256:7211ef110a9194b6042449431e08c4d80c0481e5891e58d429df5899690511c2",
|
||||||
|
"sha256:764226ceef3125e53ea2cb275000e309c0aa5464d43bd72abd661e27fffc26ab",
|
||||||
|
"sha256:7645a8e814a3ee34a89c4a372011dcd817964ce8cb273c8ed6119d706e9613e3",
|
||||||
|
"sha256:76d4711f6f6d08551a7e9ef28c722f4a50dd0fc204c56b4bcd95c6cc05ce6fbb",
|
||||||
|
"sha256:7f4f399d28478d1f604c2ff9119907af9726aed73680e5ed1ca634d377abb087",
|
||||||
|
"sha256:88f7bc0fcca81f985f78dd0fa68d2c75abf8272b1f5c323ea4a01a4d7a614efc",
|
||||||
|
"sha256:8d0edd6b1c7fb94922bf569c9b092ee187a83f03fb1a63076e7774b60f9481a8",
|
||||||
|
"sha256:901289d524fdd571be1c7be054f48b1f88ce8dddcbdf1ec698b27d4b8b9e5d62",
|
||||||
|
"sha256:93ea75c050c5bb3d98016b4ba2497851eadf0ac154d88a67d7a6816206f6fa7f",
|
||||||
|
"sha256:981b9ab5a0a3178ff413bca62526bb784249421c24ad7381e39d67981be2c326",
|
||||||
|
"sha256:9ac08e601308e41eb533f232dbf6b7e4cea762f9f84f6357136eed926c15d12c",
|
||||||
|
"sha256:a02eb8ab2b8f200179b5f62b59757685ae9987996ae549ccf30f983f40602431",
|
||||||
|
"sha256:a0c6da9aee33ff0b1a451e867da0c1f47408112b3391dd43133838339e410963",
|
||||||
|
"sha256:a6c8097e01886188e5be3e6b14e94ab365f384736aa1fca6a0b9e35bd4a30bc7",
|
||||||
|
"sha256:aa384489fefeb62321b238e64c07ef48398fe80f9e1e6afeff22e140e0850eef",
|
||||||
|
"sha256:ad2a9eb6d9839ae241701d0918f54c51365a51407fd80f6b8289e2dfca977cc3",
|
||||||
|
"sha256:b206646d176a007466358aa21d85cd8600a415c67c9bd15403336c331a10d956",
|
||||||
|
"sha256:b826d97e4276750beca7c8f0f1a4938892697a6bcd8ec8217b3312dad6982781",
|
||||||
|
"sha256:b89ac9768b82205936771f8d2eb3ce88503b1556324c9f903e7156669f521472",
|
||||||
|
"sha256:bd7bd3b3830247580de99c99ea2a01416dfc3c34471ca1298bccabf86d0ff4dc",
|
||||||
|
"sha256:bdf1847068c362f16b353163391210269e4f0569a3c166bc6a9f74ccbfc7e839",
|
||||||
|
"sha256:c11b0746f5d946fecf750428a95f3e9ebe792c1ee3b1e96eeba145dc631a9672",
|
||||||
|
"sha256:c5374b80521d3d3f2ec5572e05adc94601985cc526fb276d0c8574a6d749f1b3",
|
||||||
|
"sha256:ca265542ca427bf97aed183c1676e2a9c66942e822b14dc6e5f42e038f92a503",
|
||||||
|
"sha256:ce31ae3e19f3c902de379cf1323d90c649425b86de7bbdf82871b8a2a0615f3d",
|
||||||
|
"sha256:ceb6ec0a10c65540421e20ebd29083c50e6d1143278746a4ef6bcf6153171eb8",
|
||||||
|
"sha256:d081f13b095d74b67d550de04df1c756831f3b83dc9881c38985834387487f1b",
|
||||||
|
"sha256:d5655a942f5f5d2c9ed93d72148226d75369b4f6952680211972a33e59b1dfdc",
|
||||||
|
"sha256:d5a32087d720c608f42caed0ef36d2b3ea61a9d09ee59a5142d6070da9041b8f",
|
||||||
|
"sha256:d6484756b12f40003c6128bfcc3fa9f0d49a687e171186c2d85ec82e3758c559",
|
||||||
|
"sha256:dd65632acaf0d47608190a71bfe46b209719bf2beb59507db08ccdbe712f969b",
|
||||||
|
"sha256:de343e75f40e972bae1ef6090267f8260c1446a1695e77096db6cfa25e759a95",
|
||||||
|
"sha256:e29cda763f752553fa14c68fb2195150bfab22b352572cb36c43c47bedba70eb",
|
||||||
|
"sha256:e41f3de4df3e80de75845d3e743b3f1c4c8613c3997a912dbf0229fc61a8b963",
|
||||||
|
"sha256:e66d2a64d44d50d2543405fb183a21f76b3b5fd16f130f5c99187c3fb4e64919",
|
||||||
|
"sha256:e74b0506fa5aa5598ac6a975a12aa8928cbb58e1f5ac8360792ef15de1aa848f",
|
||||||
|
"sha256:f0ed05f5079c708fe74bf9027e95125334b6978bf07fd5ab923e9e55e5fbb9d3",
|
||||||
|
"sha256:f61e2dc5ad442c52b4887f1fdc112f97caeff4d9e6ebe78879364ac59f1663e1",
|
||||||
|
"sha256:fec520865f42e5c7f050c2a79038897b1c7d1595e907a9e08e3353293ffc948e"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.8'",
|
||||||
|
"version": "==1.4.0"
|
||||||
|
},
|
||||||
|
"idna": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4",
|
||||||
|
"sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.5'",
|
||||||
|
"version": "==3.4"
|
||||||
|
},
|
||||||
|
"jinja2": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852",
|
||||||
|
"sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==3.1.2"
|
||||||
|
},
|
||||||
|
"markupsafe": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e",
|
||||||
|
"sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e",
|
||||||
|
"sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431",
|
||||||
|
"sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686",
|
||||||
|
"sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559",
|
||||||
|
"sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc",
|
||||||
|
"sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c",
|
||||||
|
"sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0",
|
||||||
|
"sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4",
|
||||||
|
"sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9",
|
||||||
|
"sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575",
|
||||||
|
"sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba",
|
||||||
|
"sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d",
|
||||||
|
"sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3",
|
||||||
|
"sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00",
|
||||||
|
"sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155",
|
||||||
|
"sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac",
|
||||||
|
"sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52",
|
||||||
|
"sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f",
|
||||||
|
"sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8",
|
||||||
|
"sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b",
|
||||||
|
"sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24",
|
||||||
|
"sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea",
|
||||||
|
"sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198",
|
||||||
|
"sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0",
|
||||||
|
"sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee",
|
||||||
|
"sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be",
|
||||||
|
"sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2",
|
||||||
|
"sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707",
|
||||||
|
"sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6",
|
||||||
|
"sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58",
|
||||||
|
"sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779",
|
||||||
|
"sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636",
|
||||||
|
"sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c",
|
||||||
|
"sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad",
|
||||||
|
"sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee",
|
||||||
|
"sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc",
|
||||||
|
"sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2",
|
||||||
|
"sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48",
|
||||||
|
"sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7",
|
||||||
|
"sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e",
|
||||||
|
"sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b",
|
||||||
|
"sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa",
|
||||||
|
"sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5",
|
||||||
|
"sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e",
|
||||||
|
"sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb",
|
||||||
|
"sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9",
|
||||||
|
"sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57",
|
||||||
|
"sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc",
|
||||||
|
"sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==2.1.3"
|
||||||
|
},
|
||||||
|
"multidict": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9",
|
||||||
|
"sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8",
|
||||||
|
"sha256:0dfad7a5a1e39c53ed00d2dd0c2e36aed4650936dc18fd9a1826a5ae1cad6f03",
|
||||||
|
"sha256:11bdf3f5e1518b24530b8241529d2050014c884cf18b6fc69c0c2b30ca248710",
|
||||||
|
"sha256:1502e24330eb681bdaa3eb70d6358e818e8e8f908a22a1851dfd4e15bc2f8161",
|
||||||
|
"sha256:16ab77bbeb596e14212e7bab8429f24c1579234a3a462105cda4a66904998664",
|
||||||
|
"sha256:16d232d4e5396c2efbbf4f6d4df89bfa905eb0d4dc5b3549d872ab898451f569",
|
||||||
|
"sha256:21a12c4eb6ddc9952c415f24eef97e3e55ba3af61f67c7bc388dcdec1404a067",
|
||||||
|
"sha256:27c523fbfbdfd19c6867af7346332b62b586eed663887392cff78d614f9ec313",
|
||||||
|
"sha256:281af09f488903fde97923c7744bb001a9b23b039a909460d0f14edc7bf59706",
|
||||||
|
"sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2",
|
||||||
|
"sha256:3601a3cece3819534b11d4efc1eb76047488fddd0c85a3948099d5da4d504636",
|
||||||
|
"sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49",
|
||||||
|
"sha256:36c63aaa167f6c6b04ef2c85704e93af16c11d20de1d133e39de6a0e84582a93",
|
||||||
|
"sha256:39ff62e7d0f26c248b15e364517a72932a611a9b75f35b45be078d81bdb86603",
|
||||||
|
"sha256:43644e38f42e3af682690876cff722d301ac585c5b9e1eacc013b7a3f7b696a0",
|
||||||
|
"sha256:4372381634485bec7e46718edc71528024fcdc6f835baefe517b34a33c731d60",
|
||||||
|
"sha256:458f37be2d9e4c95e2d8866a851663cbc76e865b78395090786f6cd9b3bbf4f4",
|
||||||
|
"sha256:45e1ecb0379bfaab5eef059f50115b54571acfbe422a14f668fc8c27ba410e7e",
|
||||||
|
"sha256:4b9d9e4e2b37daddb5c23ea33a3417901fa7c7b3dee2d855f63ee67a0b21e5b1",
|
||||||
|
"sha256:4ceef517eca3e03c1cceb22030a3e39cb399ac86bff4e426d4fc6ae49052cc60",
|
||||||
|
"sha256:4d1a3d7ef5e96b1c9e92f973e43aa5e5b96c659c9bc3124acbbd81b0b9c8a951",
|
||||||
|
"sha256:4dcbb0906e38440fa3e325df2359ac6cb043df8e58c965bb45f4e406ecb162cc",
|
||||||
|
"sha256:509eac6cf09c794aa27bcacfd4d62c885cce62bef7b2c3e8b2e49d365b5003fe",
|
||||||
|
"sha256:52509b5be062d9eafc8170e53026fbc54cf3b32759a23d07fd935fb04fc22d95",
|
||||||
|
"sha256:52f2dffc8acaba9a2f27174c41c9e57f60b907bb9f096b36b1a1f3be71c6284d",
|
||||||
|
"sha256:574b7eae1ab267e5f8285f0fe881f17efe4b98c39a40858247720935b893bba8",
|
||||||
|
"sha256:5979b5632c3e3534e42ca6ff856bb24b2e3071b37861c2c727ce220d80eee9ed",
|
||||||
|
"sha256:59d43b61c59d82f2effb39a93c48b845efe23a3852d201ed2d24ba830d0b4cf2",
|
||||||
|
"sha256:5a4dcf02b908c3b8b17a45fb0f15b695bf117a67b76b7ad18b73cf8e92608775",
|
||||||
|
"sha256:5cad9430ab3e2e4fa4a2ef4450f548768400a2ac635841bc2a56a2052cdbeb87",
|
||||||
|
"sha256:5fc1b16f586f049820c5c5b17bb4ee7583092fa0d1c4e28b5239181ff9532e0c",
|
||||||
|
"sha256:62501642008a8b9871ddfccbf83e4222cf8ac0d5aeedf73da36153ef2ec222d2",
|
||||||
|
"sha256:64bdf1086b6043bf519869678f5f2757f473dee970d7abf6da91ec00acb9cb98",
|
||||||
|
"sha256:64da238a09d6039e3bd39bb3aee9c21a5e34f28bfa5aa22518581f910ff94af3",
|
||||||
|
"sha256:666daae833559deb2d609afa4490b85830ab0dfca811a98b70a205621a6109fe",
|
||||||
|
"sha256:67040058f37a2a51ed8ea8f6b0e6ee5bd78ca67f169ce6122f3e2ec80dfe9b78",
|
||||||
|
"sha256:6748717bb10339c4760c1e63da040f5f29f5ed6e59d76daee30305894069a660",
|
||||||
|
"sha256:6b181d8c23da913d4ff585afd1155a0e1194c0b50c54fcfe286f70cdaf2b7176",
|
||||||
|
"sha256:6ed5f161328b7df384d71b07317f4d8656434e34591f20552c7bcef27b0ab88e",
|
||||||
|
"sha256:7582a1d1030e15422262de9f58711774e02fa80df0d1578995c76214f6954988",
|
||||||
|
"sha256:7d18748f2d30f94f498e852c67d61261c643b349b9d2a581131725595c45ec6c",
|
||||||
|
"sha256:7d6ae9d593ef8641544d6263c7fa6408cc90370c8cb2bbb65f8d43e5b0351d9c",
|
||||||
|
"sha256:81a4f0b34bd92df3da93315c6a59034df95866014ac08535fc819f043bfd51f0",
|
||||||
|
"sha256:8316a77808c501004802f9beebde51c9f857054a0c871bd6da8280e718444449",
|
||||||
|
"sha256:853888594621e6604c978ce2a0444a1e6e70c8d253ab65ba11657659dcc9100f",
|
||||||
|
"sha256:99b76c052e9f1bc0721f7541e5e8c05db3941eb9ebe7b8553c625ef88d6eefde",
|
||||||
|
"sha256:a2e4369eb3d47d2034032a26c7a80fcb21a2cb22e1173d761a162f11e562caa5",
|
||||||
|
"sha256:ab55edc2e84460694295f401215f4a58597f8f7c9466faec545093045476327d",
|
||||||
|
"sha256:af048912e045a2dc732847d33821a9d84ba553f5c5f028adbd364dd4765092ac",
|
||||||
|
"sha256:b1a2eeedcead3a41694130495593a559a668f382eee0727352b9a41e1c45759a",
|
||||||
|
"sha256:b1e8b901e607795ec06c9e42530788c45ac21ef3aaa11dbd0c69de543bfb79a9",
|
||||||
|
"sha256:b41156839806aecb3641f3208c0dafd3ac7775b9c4c422d82ee2a45c34ba81ca",
|
||||||
|
"sha256:b692f419760c0e65d060959df05f2a531945af31fda0c8a3b3195d4efd06de11",
|
||||||
|
"sha256:bc779e9e6f7fda81b3f9aa58e3a6091d49ad528b11ed19f6621408806204ad35",
|
||||||
|
"sha256:bf6774e60d67a9efe02b3616fee22441d86fab4c6d335f9d2051d19d90a40063",
|
||||||
|
"sha256:c048099e4c9e9d615545e2001d3d8a4380bd403e1a0578734e0d31703d1b0c0b",
|
||||||
|
"sha256:c5cb09abb18c1ea940fb99360ea0396f34d46566f157122c92dfa069d3e0e982",
|
||||||
|
"sha256:cc8e1d0c705233c5dd0c5e6460fbad7827d5d36f310a0fadfd45cc3029762258",
|
||||||
|
"sha256:d5e3fc56f88cc98ef8139255cf8cd63eb2c586531e43310ff859d6bb3a6b51f1",
|
||||||
|
"sha256:d6aa0418fcc838522256761b3415822626f866758ee0bc6632c9486b179d0b52",
|
||||||
|
"sha256:d6c254ba6e45d8e72739281ebc46ea5eb5f101234f3ce171f0e9f5cc86991480",
|
||||||
|
"sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7",
|
||||||
|
"sha256:dcfe792765fab89c365123c81046ad4103fcabbc4f56d1c1997e6715e8015461",
|
||||||
|
"sha256:ddd3915998d93fbcd2566ddf9cf62cdb35c9e093075f862935573d265cf8f65d",
|
||||||
|
"sha256:ddff9c4e225a63a5afab9dd15590432c22e8057e1a9a13d28ed128ecf047bbdc",
|
||||||
|
"sha256:e41b7e2b59679edfa309e8db64fdf22399eec4b0b24694e1b2104fb789207779",
|
||||||
|
"sha256:e69924bfcdda39b722ef4d9aa762b2dd38e4632b3641b1d9a57ca9cd18f2f83a",
|
||||||
|
"sha256:ea20853c6dbbb53ed34cb4d080382169b6f4554d394015f1bef35e881bf83547",
|
||||||
|
"sha256:ee2a1ece51b9b9e7752e742cfb661d2a29e7bcdba2d27e66e28a99f1890e4fa0",
|
||||||
|
"sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171",
|
||||||
|
"sha256:f70b98cd94886b49d91170ef23ec5c0e8ebb6f242d734ed7ed677b24d50c82cf",
|
||||||
|
"sha256:fc35cb4676846ef752816d5be2193a1e8367b4c1397b74a565a9d0389c433a1d",
|
||||||
|
"sha256:ff959bee35038c4624250473988b24f846cbeb2c6639de3602c073f10410ceba"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==6.0.4"
|
||||||
|
},
|
||||||
|
"pillow": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:00e65f5e822decd501e374b0650146063fbb30a7264b4d2744bdd7b913e0cab5",
|
||||||
|
"sha256:040586f7d37b34547153fa383f7f9aed68b738992380ac911447bb78f2abe530",
|
||||||
|
"sha256:0b6eb5502f45a60a3f411c63187db83a3d3107887ad0d036c13ce836f8a36f1d",
|
||||||
|
"sha256:1ce91b6ec08d866b14413d3f0bbdea7e24dfdc8e59f562bb77bc3fe60b6144ca",
|
||||||
|
"sha256:1f62406a884ae75fb2f818694469519fb685cc7eaff05d3451a9ebe55c646891",
|
||||||
|
"sha256:22c10cc517668d44b211717fd9775799ccec4124b9a7f7b3635fc5386e584992",
|
||||||
|
"sha256:3400aae60685b06bb96f99a21e1ada7bc7a413d5f49bce739828ecd9391bb8f7",
|
||||||
|
"sha256:349930d6e9c685c089284b013478d6f76e3a534e36ddfa912cde493f235372f3",
|
||||||
|
"sha256:368ab3dfb5f49e312231b6f27b8820c823652b7cd29cfbd34090565a015e99ba",
|
||||||
|
"sha256:38250a349b6b390ee6047a62c086d3817ac69022c127f8a5dc058c31ccef17f3",
|
||||||
|
"sha256:3a684105f7c32488f7153905a4e3015a3b6c7182e106fe3c37fbb5ef3e6994c3",
|
||||||
|
"sha256:3a82c40d706d9aa9734289740ce26460a11aeec2d9c79b7af87bb35f0073c12f",
|
||||||
|
"sha256:3b08d4cc24f471b2c8ca24ec060abf4bebc6b144cb89cba638c720546b1cf538",
|
||||||
|
"sha256:3ed64f9ca2f0a95411e88a4efbd7a29e5ce2cea36072c53dd9d26d9c76f753b3",
|
||||||
|
"sha256:3f07ea8d2f827d7d2a49ecf1639ec02d75ffd1b88dcc5b3a61bbb37a8759ad8d",
|
||||||
|
"sha256:520f2a520dc040512699f20fa1c363eed506e94248d71f85412b625026f6142c",
|
||||||
|
"sha256:5c6e3df6bdd396749bafd45314871b3d0af81ff935b2d188385e970052091017",
|
||||||
|
"sha256:608bfdee0d57cf297d32bcbb3c728dc1da0907519d1784962c5f0c68bb93e5a3",
|
||||||
|
"sha256:685ac03cc4ed5ebc15ad5c23bc555d68a87777586d970c2c3e216619a5476223",
|
||||||
|
"sha256:76de421f9c326da8f43d690110f0e79fe3ad1e54be811545d7d91898b4c8493e",
|
||||||
|
"sha256:76edb0a1fa2b4745fb0c99fb9fb98f8b180a1bbceb8be49b087e0b21867e77d3",
|
||||||
|
"sha256:7be600823e4c8631b74e4a0d38384c73f680e6105a7d3c6824fcf226c178c7e6",
|
||||||
|
"sha256:81ff539a12457809666fef6624684c008e00ff6bf455b4b89fd00a140eecd640",
|
||||||
|
"sha256:88af2003543cc40c80f6fca01411892ec52b11021b3dc22ec3bc9d5afd1c5334",
|
||||||
|
"sha256:8c11160913e3dd06c8ffdb5f233a4f254cb449f4dfc0f8f4549eda9e542c93d1",
|
||||||
|
"sha256:8f8182b523b2289f7c415f589118228d30ac8c355baa2f3194ced084dac2dbba",
|
||||||
|
"sha256:9211e7ad69d7c9401cfc0e23d49b69ca65ddd898976d660a2fa5904e3d7a9baa",
|
||||||
|
"sha256:92be919bbc9f7d09f7ae343c38f5bb21c973d2576c1d45600fce4b74bafa7ac0",
|
||||||
|
"sha256:9c82b5b3e043c7af0d95792d0d20ccf68f61a1fec6b3530e718b688422727396",
|
||||||
|
"sha256:9f7c16705f44e0504a3a2a14197c1f0b32a95731d251777dcb060aa83022cb2d",
|
||||||
|
"sha256:9fb218c8a12e51d7ead2a7c9e101a04982237d4855716af2e9499306728fb485",
|
||||||
|
"sha256:a74ba0c356aaa3bb8e3eb79606a87669e7ec6444be352870623025d75a14a2bf",
|
||||||
|
"sha256:b4f69b3700201b80bb82c3a97d5e9254084f6dd5fb5b16fc1a7b974260f89f43",
|
||||||
|
"sha256:bc2ec7c7b5d66b8ec9ce9f720dbb5fa4bace0f545acd34870eff4a369b44bf37",
|
||||||
|
"sha256:c189af0545965fa8d3b9613cfdb0cd37f9d71349e0f7750e1fd704648d475ed2",
|
||||||
|
"sha256:c1fbe7621c167ecaa38ad29643d77a9ce7311583761abf7836e1510c580bf3dd",
|
||||||
|
"sha256:c7cf14a27b0d6adfaebb3ae4153f1e516df54e47e42dcc073d7b3d76111a8d86",
|
||||||
|
"sha256:c9f72a021fbb792ce98306ffb0c348b3c9cb967dce0f12a49aa4c3d3fdefa967",
|
||||||
|
"sha256:cd25d2a9d2b36fcb318882481367956d2cf91329f6892fe5d385c346c0649629",
|
||||||
|
"sha256:ce543ed15570eedbb85df19b0a1a7314a9c8141a36ce089c0a894adbfccb4568",
|
||||||
|
"sha256:ce7b031a6fc11365970e6a5686d7ba8c63e4c1cf1ea143811acbb524295eabed",
|
||||||
|
"sha256:d35e3c8d9b1268cbf5d3670285feb3528f6680420eafe35cccc686b73c1e330f",
|
||||||
|
"sha256:d50b6aec14bc737742ca96e85d6d0a5f9bfbded018264b3b70ff9d8c33485551",
|
||||||
|
"sha256:d5d0dae4cfd56969d23d94dc8e89fb6a217be461c69090768227beb8ed28c0a3",
|
||||||
|
"sha256:d5db32e2a6ccbb3d34d87c87b432959e0db29755727afb37290e10f6e8e62614",
|
||||||
|
"sha256:d72e2ecc68a942e8cf9739619b7f408cc7b272b279b56b2c83c6123fcfa5cdff",
|
||||||
|
"sha256:d737a602fbd82afd892ca746392401b634e278cb65d55c4b7a8f48e9ef8d008d",
|
||||||
|
"sha256:d80cf684b541685fccdd84c485b31ce73fc5c9b5d7523bf1394ce134a60c6883",
|
||||||
|
"sha256:db24668940f82321e746773a4bc617bfac06ec831e5c88b643f91f122a785684",
|
||||||
|
"sha256:dbc02381779d412145331789b40cc7b11fdf449e5d94f6bc0b080db0a56ea3f0",
|
||||||
|
"sha256:dffe31a7f47b603318c609f378ebcd57f1554a3a6a8effbc59c3c69f804296de",
|
||||||
|
"sha256:edf4392b77bdc81f36e92d3a07a5cd072f90253197f4a52a55a8cec48a12483b",
|
||||||
|
"sha256:efe8c0681042536e0d06c11f48cebe759707c9e9abf880ee213541c5b46c5bf3",
|
||||||
|
"sha256:f31f9fdbfecb042d046f9d91270a0ba28368a723302786c0009ee9b9f1f60199",
|
||||||
|
"sha256:f88a0b92277de8e3ca715a0d79d68dc82807457dae3ab8699c758f07c20b3c51",
|
||||||
|
"sha256:faaf07ea35355b01a35cb442dd950d8f1bb5b040a7787791a535de13db15ed90"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==10.0.0"
|
||||||
|
},
|
||||||
|
"yarl": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:04ab9d4b9f587c06d801c2abfe9317b77cdf996c65a90d5e84ecc45010823571",
|
||||||
|
"sha256:066c163aec9d3d073dc9ffe5dd3ad05069bcb03fcaab8d221290ba99f9f69ee3",
|
||||||
|
"sha256:13414591ff516e04fcdee8dc051c13fd3db13b673c7a4cb1350e6b2ad9639ad3",
|
||||||
|
"sha256:149ddea5abf329752ea5051b61bd6c1d979e13fbf122d3a1f9f0c8be6cb6f63c",
|
||||||
|
"sha256:159d81f22d7a43e6eabc36d7194cb53f2f15f498dbbfa8edc8a3239350f59fe7",
|
||||||
|
"sha256:1b1bba902cba32cdec51fca038fd53f8beee88b77efc373968d1ed021024cc04",
|
||||||
|
"sha256:22a94666751778629f1ec4280b08eb11815783c63f52092a5953faf73be24191",
|
||||||
|
"sha256:2a96c19c52ff442a808c105901d0bdfd2e28575b3d5f82e2f5fd67e20dc5f4ea",
|
||||||
|
"sha256:2b0738fb871812722a0ac2154be1f049c6223b9f6f22eec352996b69775b36d4",
|
||||||
|
"sha256:2c315df3293cd521033533d242d15eab26583360b58f7ee5d9565f15fee1bef4",
|
||||||
|
"sha256:32f1d071b3f362c80f1a7d322bfd7b2d11e33d2adf395cc1dd4df36c9c243095",
|
||||||
|
"sha256:3458a24e4ea3fd8930e934c129b676c27452e4ebda80fbe47b56d8c6c7a63a9e",
|
||||||
|
"sha256:38a3928ae37558bc1b559f67410df446d1fbfa87318b124bf5032c31e3447b74",
|
||||||
|
"sha256:3da8a678ca8b96c8606bbb8bfacd99a12ad5dd288bc6f7979baddd62f71c63ef",
|
||||||
|
"sha256:494053246b119b041960ddcd20fd76224149cfea8ed8777b687358727911dd33",
|
||||||
|
"sha256:50f33040f3836e912ed16d212f6cc1efb3231a8a60526a407aeb66c1c1956dde",
|
||||||
|
"sha256:52a25809fcbecfc63ac9ba0c0fb586f90837f5425edfd1ec9f3372b119585e45",
|
||||||
|
"sha256:53338749febd28935d55b41bf0bcc79d634881195a39f6b2f767870b72514caf",
|
||||||
|
"sha256:5415d5a4b080dc9612b1b63cba008db84e908b95848369aa1da3686ae27b6d2b",
|
||||||
|
"sha256:5610f80cf43b6202e2c33ba3ec2ee0a2884f8f423c8f4f62906731d876ef4fac",
|
||||||
|
"sha256:566185e8ebc0898b11f8026447eacd02e46226716229cea8db37496c8cdd26e0",
|
||||||
|
"sha256:56ff08ab5df8429901ebdc5d15941b59f6253393cb5da07b4170beefcf1b2528",
|
||||||
|
"sha256:59723a029760079b7d991a401386390c4be5bfec1e7dd83e25a6a0881859e716",
|
||||||
|
"sha256:5fcd436ea16fee7d4207c045b1e340020e58a2597301cfbcfdbe5abd2356c2fb",
|
||||||
|
"sha256:61016e7d582bc46a5378ffdd02cd0314fb8ba52f40f9cf4d9a5e7dbef88dee18",
|
||||||
|
"sha256:63c48f6cef34e6319a74c727376e95626f84ea091f92c0250a98e53e62c77c72",
|
||||||
|
"sha256:646d663eb2232d7909e6601f1a9107e66f9791f290a1b3dc7057818fe44fc2b6",
|
||||||
|
"sha256:662e6016409828ee910f5d9602a2729a8a57d74b163c89a837de3fea050c7582",
|
||||||
|
"sha256:674ca19cbee4a82c9f54e0d1eee28116e63bc6fd1e96c43031d11cbab8b2afd5",
|
||||||
|
"sha256:6a5883464143ab3ae9ba68daae8e7c5c95b969462bbe42e2464d60e7e2698368",
|
||||||
|
"sha256:6e7221580dc1db478464cfeef9b03b95c5852cc22894e418562997df0d074ccc",
|
||||||
|
"sha256:75df5ef94c3fdc393c6b19d80e6ef1ecc9ae2f4263c09cacb178d871c02a5ba9",
|
||||||
|
"sha256:783185c75c12a017cc345015ea359cc801c3b29a2966c2655cd12b233bf5a2be",
|
||||||
|
"sha256:822b30a0f22e588b32d3120f6d41e4ed021806418b4c9f0bc3048b8c8cb3f92a",
|
||||||
|
"sha256:8288d7cd28f8119b07dd49b7230d6b4562f9b61ee9a4ab02221060d21136be80",
|
||||||
|
"sha256:82aa6264b36c50acfb2424ad5ca537a2060ab6de158a5bd2a72a032cc75b9eb8",
|
||||||
|
"sha256:832b7e711027c114d79dffb92576acd1bd2decc467dec60e1cac96912602d0e6",
|
||||||
|
"sha256:838162460b3a08987546e881a2bfa573960bb559dfa739e7800ceeec92e64417",
|
||||||
|
"sha256:83fcc480d7549ccebe9415d96d9263e2d4226798c37ebd18c930fce43dfb9574",
|
||||||
|
"sha256:84e0b1599334b1e1478db01b756e55937d4614f8654311eb26012091be109d59",
|
||||||
|
"sha256:891c0e3ec5ec881541f6c5113d8df0315ce5440e244a716b95f2525b7b9f3608",
|
||||||
|
"sha256:8c2ad583743d16ddbdf6bb14b5cd76bf43b0d0006e918809d5d4ddf7bde8dd82",
|
||||||
|
"sha256:8c56986609b057b4839968ba901944af91b8e92f1725d1a2d77cbac6972b9ed1",
|
||||||
|
"sha256:8ea48e0a2f931064469bdabca50c2f578b565fc446f302a79ba6cc0ee7f384d3",
|
||||||
|
"sha256:8ec53a0ea2a80c5cd1ab397925f94bff59222aa3cf9c6da938ce05c9ec20428d",
|
||||||
|
"sha256:95d2ecefbcf4e744ea952d073c6922e72ee650ffc79028eb1e320e732898d7e8",
|
||||||
|
"sha256:9b3152f2f5677b997ae6c804b73da05a39daa6a9e85a512e0e6823d81cdad7cc",
|
||||||
|
"sha256:9bf345c3a4f5ba7f766430f97f9cc1320786f19584acc7086491f45524a551ac",
|
||||||
|
"sha256:a60347f234c2212a9f0361955007fcf4033a75bf600a33c88a0a8e91af77c0e8",
|
||||||
|
"sha256:a74dcbfe780e62f4b5a062714576f16c2f3493a0394e555ab141bf0d746bb955",
|
||||||
|
"sha256:a83503934c6273806aed765035716216cc9ab4e0364f7f066227e1aaea90b8d0",
|
||||||
|
"sha256:ac9bb4c5ce3975aeac288cfcb5061ce60e0d14d92209e780c93954076c7c4367",
|
||||||
|
"sha256:aff634b15beff8902d1f918012fc2a42e0dbae6f469fce134c8a0dc51ca423bb",
|
||||||
|
"sha256:b03917871bf859a81ccb180c9a2e6c1e04d2f6a51d953e6a5cdd70c93d4e5a2a",
|
||||||
|
"sha256:b124e2a6d223b65ba8768d5706d103280914d61f5cae3afbc50fc3dfcc016623",
|
||||||
|
"sha256:b25322201585c69abc7b0e89e72790469f7dad90d26754717f3310bfe30331c2",
|
||||||
|
"sha256:b7232f8dfbd225d57340e441d8caf8652a6acd06b389ea2d3222b8bc89cbfca6",
|
||||||
|
"sha256:b8cc1863402472f16c600e3e93d542b7e7542a540f95c30afd472e8e549fc3f7",
|
||||||
|
"sha256:b9a4e67ad7b646cd6f0938c7ebfd60e481b7410f574c560e455e938d2da8e0f4",
|
||||||
|
"sha256:be6b3fdec5c62f2a67cb3f8c6dbf56bbf3f61c0f046f84645cd1ca73532ea051",
|
||||||
|
"sha256:bf74d08542c3a9ea97bb8f343d4fcbd4d8f91bba5ec9d5d7f792dbe727f88938",
|
||||||
|
"sha256:c027a6e96ef77d401d8d5a5c8d6bc478e8042f1e448272e8d9752cb0aff8b5c8",
|
||||||
|
"sha256:c0c77533b5ed4bcc38e943178ccae29b9bcf48ffd1063f5821192f23a1bd27b9",
|
||||||
|
"sha256:c1012fa63eb6c032f3ce5d2171c267992ae0c00b9e164efe4d73db818465fac3",
|
||||||
|
"sha256:c3a53ba34a636a256d767c086ceb111358876e1fb6b50dfc4d3f4951d40133d5",
|
||||||
|
"sha256:d4e2c6d555e77b37288eaf45b8f60f0737c9efa3452c6c44626a5455aeb250b9",
|
||||||
|
"sha256:de119f56f3c5f0e2fb4dee508531a32b069a5f2c6e827b272d1e0ff5ac040333",
|
||||||
|
"sha256:e65610c5792870d45d7b68c677681376fcf9cc1c289f23e8e8b39c1485384185",
|
||||||
|
"sha256:e9fdc7ac0d42bc3ea78818557fab03af6181e076a2944f43c38684b4b6bed8e3",
|
||||||
|
"sha256:ee4afac41415d52d53a9833ebae7e32b344be72835bbb589018c9e938045a560",
|
||||||
|
"sha256:f364d3480bffd3aa566e886587eaca7c8c04d74f6e8933f3f2c996b7f09bee1b",
|
||||||
|
"sha256:f3b078dbe227f79be488ffcfc7a9edb3409d018e0952cf13f15fd6512847f3f7",
|
||||||
|
"sha256:f4e2d08f07a3d7d3e12549052eb5ad3eab1c349c53ac51c209a0e5991bbada78",
|
||||||
|
"sha256:f7a3d8146575e08c29ed1cd287068e6d02f1c7bdff8970db96683b9591b86ee7"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==1.9.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"develop": {}
|
||||||
|
}
|
104
README.md
Normal file
104
README.md
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
# rsnaps
|
||||||
|
|
||||||
|
Capture a raw snap stream (scans or pictures) through a locally running webapp.
|
||||||
|
|
||||||
|
*rsnaps* is a simple webapp for local deployment on a machine with access to
|
||||||
|
one or more scanner devices, or cameras with Canon CHDK.
|
||||||
|
Users can start scan jobs and the scanned images will be put into
|
||||||
|
a local directory with a serial number in the name.
|
||||||
|
Users can see the list of previously scanned images and redo previous scans.
|
||||||
|
|
||||||
|
The scanned images are stored in PNG format and few metadata are added to them:
|
||||||
|
|
||||||
|
* the name of the operator doing the scan
|
||||||
|
* the ID of the unit/batch of material that is being scanned
|
||||||
|
* the date and time of the scan
|
||||||
|
* the device used for scanning
|
||||||
|
|
||||||
|
Limitations:
|
||||||
|
|
||||||
|
* no concurrency: only one instance of rsnaps can run on a machine
|
||||||
|
|
||||||
|
* A4 (181x256)
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Debian Linux with these packages installed
|
||||||
|
|
||||||
|
* python3-sane
|
||||||
|
* python3-aiohttp
|
||||||
|
* python3-aiohttp-jinja2
|
||||||
|
|
||||||
|
```
|
||||||
|
apt install sane python3-sane python3-aiohttp python3-aiohttp-jinja2
|
||||||
|
```
|
||||||
|
|
||||||
|
### PDF creation
|
||||||
|
|
||||||
|
Install jbig2 from https://github.com/agl/jbig2enc
|
||||||
|
(cf. https://ocrmypdf.readthedocs.io/en/latest/jbig2.html).
|
||||||
|
|
||||||
|
You'll need these dependencies: TODO
|
||||||
|
|
||||||
|
Note: For jbig2 compression architecture amd64 is required.
|
||||||
|
|
||||||
|
### OCR
|
||||||
|
|
||||||
|
`apt install pngquant ocrmypdf`
|
||||||
|
and required tesseract language packages, e.g. `tesseract-ocr-eng`, `tesseract-ocr-deu`, ...
|
||||||
|
|
||||||
|
### Page rotationa
|
||||||
|
|
||||||
|
`apt install imagemagick`
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
### User and software
|
||||||
|
|
||||||
|
```
|
||||||
|
adduser --disabled-login --home /srv/rsnaps --ingroup scanner
|
||||||
|
su - rsnaps
|
||||||
|
git clone _______TODO_________ repo
|
||||||
|
|
||||||
|
... TODO
|
||||||
|
```
|
||||||
|
|
||||||
|
### systemd integration
|
||||||
|
|
||||||
|
Put a systemd service unit in `/etc/systemd/system/rsnaps.service`:
|
||||||
|
```
|
||||||
|
[Unit]
|
||||||
|
Description=rsnaps local sanning service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=
|
||||||
|
ExecStart=
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable rsnaps.service
|
||||||
|
systemctl start rsnaps.service
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
Useful resources:
|
||||||
|
|
||||||
|
* [python-sane docs](https://python-sane.readthedocs.io/en/latest/)
|
||||||
|
* [example.py](https://github.com/python-pillow/Sane/blob/main/example.py)
|
||||||
|
* [python-sane source](https://github.com/python-pillow/Sane/)
|
||||||
|
* [SANE standard](https://sane-project.gitlab.io/standard/)
|
||||||
|
|
||||||
|
|
||||||
|
## TODO
|
||||||
|
|
||||||
|
* handle timeout of subprocess calls
|
||||||
|
* lazy image loading:
|
||||||
|
https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
|
||||||
|
https://css-tricks.com/the-complete-guide-to-lazy-loading-images/
|
||||||
|
https://web.dev/lazy-loading-images/
|
||||||
|
https://developer.mozilla.org/en-US/docs/Web/Performance/Lazy_loading
|
823
server.py
Executable file
823
server.py
Executable file
|
@ -0,0 +1,823 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
"""
|
||||||
|
Simple server for gathering images from locally connected devices.
|
||||||
|
|
||||||
|
Supported devices are scanners available through SANE on linux.
|
||||||
|
|
||||||
|
This service will no work for multiple users.
|
||||||
|
|
||||||
|
TODO: allow for shutdown and call device.close() then
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from copy import deepcopy
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import aiohttp_jinja2
|
||||||
|
import jinja2
|
||||||
|
import sane
|
||||||
|
from aiohttp import web
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
|
||||||
|
ocr_languages = ['deu', 'eng']
|
||||||
|
|
||||||
|
|
||||||
|
logging.basicConfig()
|
||||||
|
logging.getLogger().setLevel(logging.DEBUG)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
|
||||||
|
config_dir = Path.home() / '.config' / 'rsnaps'
|
||||||
|
cache_dir = Path.home() / '.cache' / 'rsnaps'
|
||||||
|
cache_dir_small = cache_dir / 'small'
|
||||||
|
cache_dir_pdf = cache_dir / 'pdf'
|
||||||
|
archive_dir = None
|
||||||
|
|
||||||
|
|
||||||
|
thumbnail_width = 181
|
||||||
|
thumbnail_height = 256
|
||||||
|
|
||||||
|
|
||||||
|
path_jbig2 = Path.home() / 'Desktop/tools/scan/jbig2'
|
||||||
|
path_pdf_py = Path.home() / 'Desktop/tools/scan/pdf.py'
|
||||||
|
|
||||||
|
|
||||||
|
routes = web.RouteTableDef()
|
||||||
|
|
||||||
|
|
||||||
|
app_basedir = Path(__file__).parent
|
||||||
|
|
||||||
|
|
||||||
|
snap_device = None
|
||||||
|
|
||||||
|
|
||||||
|
settings_cache = None
|
||||||
|
|
||||||
|
|
||||||
|
def init(archive_dir_):
|
||||||
|
"""
|
||||||
|
Setup config and cache directories.
|
||||||
|
"""
|
||||||
|
global archive_dir
|
||||||
|
try:
|
||||||
|
archive_dir = Path(archive_dir_)
|
||||||
|
except:
|
||||||
|
print('Invalid archive basedir.')
|
||||||
|
sys.exit(2)
|
||||||
|
if not archive_dir.exists():
|
||||||
|
print('Archive basedir does not exist.')
|
||||||
|
sys.exit(2)
|
||||||
|
# TODO: check if archive_dir is writable
|
||||||
|
config_dir.mkdir(mode=0o700, exist_ok=True)
|
||||||
|
cache_dir.mkdir(mode=0o700, exist_ok=True)
|
||||||
|
cache_dir_small.mkdir(mode=0o700, exist_ok=True)
|
||||||
|
cache_dir_pdf.mkdir(mode=0o700, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def update_settings(settings: dict):
|
||||||
|
"""
|
||||||
|
Update existing settings, store and return them.
|
||||||
|
|
||||||
|
Fully replace keys that are in *settings*.
|
||||||
|
"""
|
||||||
|
settings_path = config_dir / 'settings.json'
|
||||||
|
try:
|
||||||
|
with open(settings_path, 'r') as file:
|
||||||
|
settings_ = json.loads(file.read())
|
||||||
|
except:
|
||||||
|
settings_ = {}
|
||||||
|
settings_.update(settings)
|
||||||
|
with open(settings_path, 'w') as file:
|
||||||
|
file.write(json.dumps(settings_, indent=4))
|
||||||
|
global settings_cache
|
||||||
|
settings_cache = deepcopy(settings_)
|
||||||
|
return settings_
|
||||||
|
|
||||||
|
|
||||||
|
def get_settings():
|
||||||
|
"""
|
||||||
|
Return stored settings.
|
||||||
|
"""
|
||||||
|
global settings_cache
|
||||||
|
if settings_cache:
|
||||||
|
return deepcopy(settings_cache)
|
||||||
|
settings_path = config_dir / 'settings.json'
|
||||||
|
try:
|
||||||
|
with open(settings_path, 'r') as file:
|
||||||
|
setting_cache = json.loads(file.read())
|
||||||
|
return deepcopy(settings_cache or {})
|
||||||
|
except Exception as err:
|
||||||
|
logger.exception(err)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
async def store_settings(request):
|
||||||
|
"""
|
||||||
|
Extract settings from POST data and store them.
|
||||||
|
|
||||||
|
Also change global `snap_device` to the selected one.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
data = await request.post()
|
||||||
|
settings_ = get_settings()
|
||||||
|
settings = {}
|
||||||
|
|
||||||
|
# device
|
||||||
|
device_id = data.get('device_id')
|
||||||
|
device_data = None
|
||||||
|
devices = [list(x) for x in settings_.get('devices')] or []
|
||||||
|
for device_data_ in devices:
|
||||||
|
if device_id == device_data_[0]:
|
||||||
|
device_data = device_data_.copy()
|
||||||
|
global snap_device
|
||||||
|
if device_id != settings_.get('device_id') or snap_device is None:
|
||||||
|
settings['device_id'] = device_id
|
||||||
|
snap_device = sane.open(device_id)
|
||||||
|
|
||||||
|
# device_settings
|
||||||
|
device_settings = {}
|
||||||
|
if paper_size := data.get('paper_size'): # for setting the scan area
|
||||||
|
device_settings['paper_size'] = paper_size
|
||||||
|
if mode := data.get('mode'):
|
||||||
|
device_settings['mode'] = mode
|
||||||
|
if resolution := data.get('resolution'):
|
||||||
|
device_settings['resolution'] = int(resolution)
|
||||||
|
device_settings['snap'] = data.get('snap')
|
||||||
|
|
||||||
|
# collection
|
||||||
|
collection_choice = Path(settings_.get('collection_choice', '.'))
|
||||||
|
if collection_new := str(data.get('collection_new')):
|
||||||
|
p = archive_dir / collection_choice / collection_new
|
||||||
|
p.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||||
|
settings['collection_choice'] = str(collection_choice / collection_new)
|
||||||
|
if collection_description := data.get('collection_description'):
|
||||||
|
settings['collection_description'] = collection_description
|
||||||
|
|
||||||
|
# (duplex) page number
|
||||||
|
if dpage_number := data.get('dpage_number'):
|
||||||
|
try:
|
||||||
|
dpage_number = int(dpage_number)
|
||||||
|
except:
|
||||||
|
dpage_number = None
|
||||||
|
if dpage_number in (None, ''):
|
||||||
|
max_ = 0
|
||||||
|
for name in archive_dir.glob('*'):
|
||||||
|
try:
|
||||||
|
max_ = max(max_, int(name[:4]))
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
dpage_number = max_ + 1
|
||||||
|
settings['dpage_number'] = dpage_number
|
||||||
|
|
||||||
|
# target
|
||||||
|
settings['target'] = data.get('target', 'a')
|
||||||
|
|
||||||
|
# save settings, if changed
|
||||||
|
settings_old = deepcopy(settings_)
|
||||||
|
settings_.update(settings)
|
||||||
|
if 'device_settings' not in settings_:
|
||||||
|
settings_['device_settings'] = {}
|
||||||
|
if device_id and device_id not in settings_['device_settings']:
|
||||||
|
settings_['device_settings'][device_id] = {}
|
||||||
|
settings_['device_settings'][device_id].update(device_settings)
|
||||||
|
if settings_old != settings_:
|
||||||
|
settings_path = config_dir / 'settings.json'
|
||||||
|
with open(settings_path, 'w') as file:
|
||||||
|
file.write(json.dumps(settings_, indent=4))
|
||||||
|
global settings_cache
|
||||||
|
settings_cache = deepcopy(settings_)
|
||||||
|
return settings_
|
||||||
|
except Exception as err:
|
||||||
|
logger.exception(err)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def get_params(settings=None):
|
||||||
|
"""
|
||||||
|
Return params required for main template (snaps.html).
|
||||||
|
|
||||||
|
Includes settings and image names.
|
||||||
|
"""
|
||||||
|
if settings is None:
|
||||||
|
settings = get_settings()
|
||||||
|
images = []
|
||||||
|
image_path = archive_dir / settings.get('collection_choice', '.')
|
||||||
|
for path in image_path.glob('*'):
|
||||||
|
if path.is_file():
|
||||||
|
name = path.with_suffix('').name
|
||||||
|
images.append(name)
|
||||||
|
device_id = settings.get('device_id')
|
||||||
|
device_settings = settings.get('device_settings', {}).get(device_id, {})
|
||||||
|
collection_choices = [str(d.relative_to(archive_dir))
|
||||||
|
for d in archive_dir.glob('**') if d.is_dir()]
|
||||||
|
collection_choices.sort(key=lambda x: x.lower())
|
||||||
|
return {
|
||||||
|
'devices': settings.get('devices', []),
|
||||||
|
'device_id': device_id,
|
||||||
|
'paper_sizes': ['DIN A4 (left)', 'DIN A5 (left)', 'DIN A5 (centered)', 'Letter', '115x158mm', '145x420mm', '157x240mm', '170x240mm', '210x440mm'],
|
||||||
|
'paper_size': device_settings.get('paper_size', 'DIN A4 (left)'),
|
||||||
|
'modes': device_settings.get('modes', []),
|
||||||
|
'mode': device_settings.get('mode', 'Gray'),
|
||||||
|
'resolutions': device_settings.get('resolutions', []),
|
||||||
|
'resolution': device_settings.get('resolution', 300),
|
||||||
|
'sources': device_settings.get('sources', []),
|
||||||
|
'collection_choices': collection_choices,
|
||||||
|
'collection_choice': settings.get('collection_choice', '.'),
|
||||||
|
'collection_description': settings.get('collection_description', ''),
|
||||||
|
'dpage_number': settings.get('dpage_number', 0) + 1,
|
||||||
|
'target': settings.get('target', 'a'),
|
||||||
|
'images': sorted(images),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@aiohttp_jinja2.template('rsnaps.html')
|
||||||
|
async def detect(request):
|
||||||
|
"""
|
||||||
|
Detect available devices and store them in settings.
|
||||||
|
"""
|
||||||
|
sane.exit()
|
||||||
|
sane.init()
|
||||||
|
devices = sane.get_devices()
|
||||||
|
sane.exit()
|
||||||
|
update_settings({'devices': devices})
|
||||||
|
raise web.HTTPFound('/')
|
||||||
|
|
||||||
|
|
||||||
|
@aiohttp_jinja2.template('rsnaps.html')
|
||||||
|
async def collection(request):
|
||||||
|
data = await request.post()
|
||||||
|
collection_choice = data.get('collection_choice', '.')
|
||||||
|
update_settings({'collection_choice': collection_choice})
|
||||||
|
raise web.HTTPFound('/')
|
||||||
|
|
||||||
|
|
||||||
|
@aiohttp_jinja2.template('rsnaps.html')
|
||||||
|
async def collection_delete(request):
|
||||||
|
data = await request.post()
|
||||||
|
collection_choice = data.get('collection_choice', '.')
|
||||||
|
if collection_choice and collection_choice != '.':
|
||||||
|
image_dir = archive_dir / collection_choice
|
||||||
|
remove_dir(image_dir)
|
||||||
|
thumbnail_dir = cache_dir_small / collection_choice
|
||||||
|
remove_dir(thumbnail_dir)
|
||||||
|
else:
|
||||||
|
print(76576567373, collection_choice) # TODO
|
||||||
|
update_settings({'collection_choice': '.'})
|
||||||
|
raise web.HTTPFound('/')
|
||||||
|
|
||||||
|
|
||||||
|
def remove_dir(dir_path):
|
||||||
|
if not dir_path.is_dir():
|
||||||
|
return
|
||||||
|
has_subdirs = False
|
||||||
|
for p in dir_path.iterdir():
|
||||||
|
if p.is_file():
|
||||||
|
p.unlink()
|
||||||
|
else:
|
||||||
|
has_subdirs = True
|
||||||
|
if not has_subdirs:
|
||||||
|
dir_path.rmdir()
|
||||||
|
|
||||||
|
|
||||||
|
@aiohttp_jinja2.template('rsnaps.html')
|
||||||
|
async def device(request):
|
||||||
|
data = await request.post()
|
||||||
|
device_id = data.get('device_id')
|
||||||
|
global snap_device
|
||||||
|
if device_id:
|
||||||
|
try:
|
||||||
|
snap_device = sane.open(device_id)
|
||||||
|
except:
|
||||||
|
try:
|
||||||
|
sane.exit()
|
||||||
|
if ':libusb:' in device_id:
|
||||||
|
bus_devnum = device_id[-7:]
|
||||||
|
# get usb_ids
|
||||||
|
cmd = ['lsusb', '-s', bus_devnum]
|
||||||
|
process = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
|
||||||
|
out, _ = process.communicate()
|
||||||
|
usb_ids = out.decode('utf-8').split(' ')[5]
|
||||||
|
# do usbreset
|
||||||
|
cmd = ['usbreset', usb_ids]
|
||||||
|
subprocess.run(cmd)
|
||||||
|
sane.init()
|
||||||
|
try:
|
||||||
|
snap_device = sane.open(device_id)
|
||||||
|
except Exception as err:
|
||||||
|
print(f'Error opening device {device_id}: {err}')
|
||||||
|
snap_device = None
|
||||||
|
except:
|
||||||
|
print(f'Error on sane exit and init')
|
||||||
|
snap_device = None
|
||||||
|
else:
|
||||||
|
snap_device = None
|
||||||
|
if snap_device:
|
||||||
|
update_settings({'device_id': device_id})
|
||||||
|
else:
|
||||||
|
update_settings({'device_id': None})
|
||||||
|
|
||||||
|
# get and store device constraints
|
||||||
|
if snap_device:
|
||||||
|
modes = snap_device['mode'].constraint
|
||||||
|
resolutions = snap_device['resolution'].constraint
|
||||||
|
sources = snap_device['source'].constraint
|
||||||
|
device_settings = {}
|
||||||
|
device_settings['modes'] = modes
|
||||||
|
device_settings['resolutions'] = resolutions
|
||||||
|
device_settings['sources'] = sources
|
||||||
|
settings = get_settings()
|
||||||
|
if 'device_settings' not in settings:
|
||||||
|
settings['device_settings'] = {}
|
||||||
|
if device_id not in settings['device_settings']:
|
||||||
|
settings['device_settings'][device_id] = {}
|
||||||
|
settings['device_settings'][device_id].update(device_settings)
|
||||||
|
update_settings({'device_settings': settings['device_settings']})
|
||||||
|
|
||||||
|
params = snap_device.get_parameters()
|
||||||
|
logger.info(f'Scanner device parameters: {params}')
|
||||||
|
# logger.info(f'Scanner device options: {snap_device.optlist}')
|
||||||
|
# logger.info(dir(snap_device))
|
||||||
|
|
||||||
|
# for o in snap_device.optlist:
|
||||||
|
# try:
|
||||||
|
# print(snap_device[o], dir(snap_device[o]))
|
||||||
|
# print(o, snap_device[o].constraint)
|
||||||
|
# except:
|
||||||
|
# pass
|
||||||
|
|
||||||
|
raise web.HTTPFound('/')
|
||||||
|
|
||||||
|
|
||||||
|
@aiohttp_jinja2.template('rsnaps.html')
|
||||||
|
async def rsnaps(request):
|
||||||
|
"""
|
||||||
|
Just display main view.
|
||||||
|
"""
|
||||||
|
return get_params()
|
||||||
|
|
||||||
|
|
||||||
|
@aiohttp_jinja2.template('rsnaps.html')
|
||||||
|
async def snap(request):
|
||||||
|
"""
|
||||||
|
Perform a snap.
|
||||||
|
"""
|
||||||
|
settings = await store_settings(request)
|
||||||
|
try:
|
||||||
|
snap_page(settings)
|
||||||
|
except:
|
||||||
|
logger.exception(f'FAIL: snap_page({settings})')
|
||||||
|
return get_params(settings)
|
||||||
|
|
||||||
|
|
||||||
|
async def image_delete(request):
|
||||||
|
"""
|
||||||
|
Delete the named image.
|
||||||
|
"""
|
||||||
|
name = request.match_info['name']
|
||||||
|
settings = get_settings()
|
||||||
|
image_dir = archive_dir / settings.get('collection_choice', '.')
|
||||||
|
image_file = image_dir / f'{name}.png'
|
||||||
|
if image_file.is_file():
|
||||||
|
image_file.unlink()
|
||||||
|
image_dir_small = cache_dir_small / settings.get('collection_choice', '.')
|
||||||
|
image_file_small = image_dir_small / f'{name}.png'
|
||||||
|
if image_file_small.is_file():
|
||||||
|
image_file_small.unlink()
|
||||||
|
raise web.HTTPFound('/')
|
||||||
|
|
||||||
|
|
||||||
|
async def image(request):
|
||||||
|
"""
|
||||||
|
Display the named image.
|
||||||
|
"""
|
||||||
|
name = request.match_info['name']
|
||||||
|
settings = get_settings()
|
||||||
|
image_dir = archive_dir / settings.get('collection_choice', '.')
|
||||||
|
try:
|
||||||
|
with open(image_dir / f'{name}.png', 'rb') as file:
|
||||||
|
img_content = file.read()
|
||||||
|
return web.Response(body=img_content, content_type='image/png')
|
||||||
|
except Exception as err:
|
||||||
|
logger.exception('Image not found')
|
||||||
|
raise web.HTTPNotFound(text='The image does not exist.')
|
||||||
|
|
||||||
|
|
||||||
|
async def image_small(request):
|
||||||
|
"""
|
||||||
|
Display the named thumbnail / small image.
|
||||||
|
"""
|
||||||
|
name = request.match_info['name']
|
||||||
|
settings = get_settings()
|
||||||
|
image_dir_small = cache_dir_small / settings.get('collection_choice', '.')
|
||||||
|
image_file_small = image_dir_small / f'{name}.png'
|
||||||
|
try:
|
||||||
|
with open(image_file_small, 'rb') as file:
|
||||||
|
img_content = file.read()
|
||||||
|
return web.Response(body=img_content, content_type='image/png')
|
||||||
|
except Exception:
|
||||||
|
image_dir = archive_dir / settings.get('collection_choice', '.')
|
||||||
|
image_file = image_dir / f'{name}.png'
|
||||||
|
if not image_file.is_file():
|
||||||
|
logger.exception('Image not found')
|
||||||
|
raise web.HTTPNotFound(text='The image does not exist.')
|
||||||
|
|
||||||
|
if not image_dir_small.is_dir():
|
||||||
|
image_dir_small.mkdir(mode=0o700, parents=True)
|
||||||
|
with Image.open(image_file) as img:
|
||||||
|
img_small = img.resize((thumbnail_width, thumbnail_height), Image.BICUBIC)
|
||||||
|
img_small.save(image_dir_small / f'{name}.png')
|
||||||
|
|
||||||
|
|
||||||
|
async def pages_operation(request):
|
||||||
|
"""
|
||||||
|
Perform the requested operation on pages, e.g. PDF generation.
|
||||||
|
"""
|
||||||
|
data = await request.post()
|
||||||
|
if not (pages := parse_pages(data.get('pages'))):
|
||||||
|
raise web.HTTPFound('/')
|
||||||
|
operation = data.get('operation')
|
||||||
|
if operation == 'delete':
|
||||||
|
await delete_pages(pages)
|
||||||
|
raise web.HTTPFound('/')
|
||||||
|
elif operation == 'rotate180':
|
||||||
|
await rotate_pages(pages, angle=180)
|
||||||
|
raise web.HTTPFound('/')
|
||||||
|
else:
|
||||||
|
ocr = operation == 'pdf_ocr'
|
||||||
|
lossy = data.get('lossy') == 'lossy'
|
||||||
|
return await create_pdf(pages, ocr=ocr, lossy=lossy)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_pages(pages_):
|
||||||
|
"""
|
||||||
|
Input cleaning: Filter `pages_` for existing ones.
|
||||||
|
|
||||||
|
Return a list of pages, retaining the requested sort order.
|
||||||
|
"""
|
||||||
|
pages = []
|
||||||
|
if not pages_:
|
||||||
|
return pages
|
||||||
|
for pr in (prs := str(pages_).split(',')):
|
||||||
|
if '-' in pr:
|
||||||
|
start_, end_ = pr.split('-', 1)
|
||||||
|
start_ = start_.strip()
|
||||||
|
end_ = end_.strip()
|
||||||
|
if start_.endswith('a'):
|
||||||
|
start_t = 'a'
|
||||||
|
start_ = start_[:-1]
|
||||||
|
elif start_.endswith('b'):
|
||||||
|
start_t = 'b'
|
||||||
|
start_ = start_[:-1]
|
||||||
|
else:
|
||||||
|
start_t = ''
|
||||||
|
start = int(start_)
|
||||||
|
if end_.endswith('a'):
|
||||||
|
end_t = 'a'
|
||||||
|
end_ = end_[:-1]
|
||||||
|
elif end_.endswith('b'):
|
||||||
|
end_t = 'b'
|
||||||
|
end_ = end_[:-1]
|
||||||
|
else:
|
||||||
|
end_t = ''
|
||||||
|
end = int(end_)
|
||||||
|
else:
|
||||||
|
pr = pr.strip()
|
||||||
|
if pr.endswith('a'):
|
||||||
|
pr_t = 'a'
|
||||||
|
pr = pr[:-1]
|
||||||
|
elif pr.endswith('b'):
|
||||||
|
pr_t = 'b'
|
||||||
|
pr = pr[:-1]
|
||||||
|
else:
|
||||||
|
pr_t = ''
|
||||||
|
start = end = int(pr)
|
||||||
|
start_t = end_t = pr_t
|
||||||
|
for ind in range(start, end + 1):
|
||||||
|
if ind == start:
|
||||||
|
if not start_t or start_t == 'a':
|
||||||
|
pages.append(f'{ind:04d}a')
|
||||||
|
if not (ind == end and end_t == 'a'):
|
||||||
|
pages.append(f'{ind:04d}b')
|
||||||
|
elif ind == end:
|
||||||
|
if not (ind == start and start_t == 'b'):
|
||||||
|
pages.append(f'{ind:04d}a')
|
||||||
|
if not end_t or end_t == 'b':
|
||||||
|
pages.append(f'{ind:04d}b')
|
||||||
|
else:
|
||||||
|
pages.append(f'{ind:04d}a')
|
||||||
|
pages.append(f'{ind:04d}b')
|
||||||
|
# filter by existing pages
|
||||||
|
pages_set = set(pages)
|
||||||
|
image_dir = archive_dir / get_settings().get('collection_choice', '.')
|
||||||
|
existing = set([p.stem for p in image_dir.iterdir()])
|
||||||
|
common = pages_set & existing
|
||||||
|
return [page for page in pages if page in common]
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_pages(pages):
|
||||||
|
"""
|
||||||
|
Delete the given `pages`, i.e. images and small images.
|
||||||
|
"""
|
||||||
|
image_dir = archive_dir / get_settings().get('collection_choice', '.')
|
||||||
|
image_dir_small = cache_dir_small / get_settings().get('collection_choice', '.')
|
||||||
|
for page in pages:
|
||||||
|
p = image_dir / f'{page}.png'
|
||||||
|
p.unlink(missing_ok=True)
|
||||||
|
p = image_dir_small / f'{page}.png'
|
||||||
|
p.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
async def rotate_pages(pages, angle=0):
|
||||||
|
"""
|
||||||
|
Rotate the `pages` by `angle`, i.e. images and small images.
|
||||||
|
"""
|
||||||
|
image_dir = archive_dir / get_settings().get('collection_choice', '.')
|
||||||
|
image_dir_small = cache_dir_small / get_settings().get('collection_choice', '.')
|
||||||
|
for page in pages:
|
||||||
|
p = image_dir / f'{page}.png'
|
||||||
|
cmd = [
|
||||||
|
'mogrify',
|
||||||
|
'-rotate',
|
||||||
|
str(angle),
|
||||||
|
str(p),
|
||||||
|
]
|
||||||
|
subprocess.run(cmd, cwd=cache_dir_pdf)
|
||||||
|
p = image_dir_small / f'{page}.png'
|
||||||
|
cmd = [
|
||||||
|
'mogrify',
|
||||||
|
'-rotate',
|
||||||
|
str(angle),
|
||||||
|
str(p),
|
||||||
|
]
|
||||||
|
subprocess.run(cmd, cwd=cache_dir_pdf)
|
||||||
|
|
||||||
|
|
||||||
|
async def create_pdf(pages, lossy: bool = False, ocr: bool = False):
|
||||||
|
"""
|
||||||
|
Create and return a PDF from the given pages.
|
||||||
|
|
||||||
|
If `lossy` is True, use jbig2 for compression.
|
||||||
|
|
||||||
|
If `ocr` is True, perform OCR using `ocrmypdf`.
|
||||||
|
"""
|
||||||
|
for p in cache_dir_pdf.iterdir():
|
||||||
|
p.unlink()
|
||||||
|
|
||||||
|
img_paths = [str(archive_dir / f'{page}.png') for page in pages]
|
||||||
|
|
||||||
|
if lossy:
|
||||||
|
cmd = [
|
||||||
|
str(path_jbig2),
|
||||||
|
'-s',
|
||||||
|
'-p',
|
||||||
|
'-a',
|
||||||
|
'-v',
|
||||||
|
'-4',
|
||||||
|
] + img_paths
|
||||||
|
logger.debug(' '.join([str(x) for x in cmd]))
|
||||||
|
subprocess.run(cmd, cwd=cache_dir_pdf)
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
'/usr/bin/python3',
|
||||||
|
str(path_pdf_py),
|
||||||
|
'output',
|
||||||
|
]
|
||||||
|
logger.debug(' '.join(cmd))
|
||||||
|
subprocess.run(cmd, cwd=cache_dir_pdf, capture_output=True)
|
||||||
|
#with open(cache_dir_pdf / 'x.pdf', 'wb') as file:
|
||||||
|
# file.write(result.stdout)
|
||||||
|
else:
|
||||||
|
# create a PDF file using img2pdf
|
||||||
|
cmd = [
|
||||||
|
'img2pdf',
|
||||||
|
'--pagesize',
|
||||||
|
'A4',
|
||||||
|
'-o',
|
||||||
|
str(cache_dir_pdf / 'o1.pdf'),
|
||||||
|
] + img_paths
|
||||||
|
logger.debug(' '.join([str(x) for x in cmd]))
|
||||||
|
subprocess.run(cmd, cwd=cache_dir_pdf)
|
||||||
|
|
||||||
|
# optimize images and linearize the pdf file using qpdf
|
||||||
|
cmd = [
|
||||||
|
'qpdf',
|
||||||
|
'--optimize-images',
|
||||||
|
'--linearize',
|
||||||
|
'--compress-streams=y',
|
||||||
|
'--object-streams=generate',
|
||||||
|
'--recompress-flate',
|
||||||
|
str(cache_dir_pdf / 'o1.pdf'),
|
||||||
|
str(cache_dir_pdf / 'o.pdf'),
|
||||||
|
]
|
||||||
|
logger.debug(' '.join([str(x) for x in cmd]))
|
||||||
|
subprocess.run(cmd, cwd=cache_dir_pdf)
|
||||||
|
|
||||||
|
if ocr:
|
||||||
|
cmd = [
|
||||||
|
'ocrmypdf',
|
||||||
|
'-d',
|
||||||
|
'-O',
|
||||||
|
'3',
|
||||||
|
'-l',
|
||||||
|
'+'.join(ocr_languages),
|
||||||
|
'--output-type',
|
||||||
|
'pdf',
|
||||||
|
cache_dir_pdf / 'o.pdf',
|
||||||
|
cache_dir_pdf / 'ocr.pdf',
|
||||||
|
]
|
||||||
|
logger.debug(' '.join([str(x) for x in cmd]))
|
||||||
|
subprocess.run(cmd, cwd=cache_dir_pdf)
|
||||||
|
result_file = cache_dir_pdf / 'ocr.pdf'
|
||||||
|
else:
|
||||||
|
result_file = cache_dir_pdf / 'o.pdf'
|
||||||
|
|
||||||
|
# return result
|
||||||
|
with open(result_file, 'rb') as file:
|
||||||
|
result_content = file.read()
|
||||||
|
return web.Response(body=result_content, content_type='application/pdf')
|
||||||
|
|
||||||
|
|
||||||
|
app = web.Application()
|
||||||
|
app.add_routes([
|
||||||
|
web.get('/', rsnaps),
|
||||||
|
web.post('/detect', detect),
|
||||||
|
web.post('/collection', collection),
|
||||||
|
web.post('/collection/delete', collection_delete),
|
||||||
|
web.post('/device', device),
|
||||||
|
web.post('/snap', snap),
|
||||||
|
web.post('/image-delete/{name}', image_delete),
|
||||||
|
web.get('/image/{name}', image),
|
||||||
|
web.get('/image-small/{name}', image_small),
|
||||||
|
web.post('/pages-operation', pages_operation),
|
||||||
|
])
|
||||||
|
aiohttp_jinja2.setup(
|
||||||
|
app,
|
||||||
|
loader=jinja2.FileSystemLoader(app_basedir / 'templates'),
|
||||||
|
)
|
||||||
|
app.router.add_static('/static', app_basedir / 'static')
|
||||||
|
|
||||||
|
|
||||||
|
# snap data
|
||||||
|
|
||||||
|
|
||||||
|
def snap_page(settings):
|
||||||
|
"""
|
||||||
|
Set device options.
|
||||||
|
"""
|
||||||
|
device_id = settings.get('device_id')
|
||||||
|
device_settings = settings.get('device_settings', {}).get(device_id, {})
|
||||||
|
|
||||||
|
# set source before mode and resolution!
|
||||||
|
global snap_device
|
||||||
|
snap_device.source = device_settings.get('snap')
|
||||||
|
snap_device.mode = device_settings.get('mode')
|
||||||
|
snap_device.resolution = device_settings.get('resolution')
|
||||||
|
|
||||||
|
if device_settings.get('snap') in ('Automatic Document Feeder', 'ADF Front'):
|
||||||
|
scan_adf(snap_device, settings)
|
||||||
|
if device_settings.get('snap') == 'ADF Duplex':
|
||||||
|
scan_adf_duplex(snap_device, settings)
|
||||||
|
else:
|
||||||
|
scan_page(snap_device, settings)
|
||||||
|
|
||||||
|
|
||||||
|
def scan_page(snap_device, settings):
|
||||||
|
"""
|
||||||
|
Scan a single page from source 'Flatbed'.
|
||||||
|
"""
|
||||||
|
set_scan_area(settings)
|
||||||
|
snap_device.start()
|
||||||
|
img = snap_device.snap()
|
||||||
|
store_image(img, settings)
|
||||||
|
|
||||||
|
|
||||||
|
def scan_adf(snap_device, settings):
|
||||||
|
"""
|
||||||
|
Scan pages from source 'Automatic Document Feeder'.
|
||||||
|
"""
|
||||||
|
set_scan_area(settings)
|
||||||
|
direction = -1 if settings.get('target') == 'b' else 1
|
||||||
|
img_i = 0
|
||||||
|
dpage_number = settings.get('dpage_number', 1)
|
||||||
|
if direction == -1:
|
||||||
|
dpage_number -= 1
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
snap_device.start()
|
||||||
|
img = snap_device.snap(True)
|
||||||
|
if not isinstance(img, Image.Image):
|
||||||
|
break
|
||||||
|
settings['dpage_number'] = dpage_number + direction * img_i
|
||||||
|
store_image(img, settings)
|
||||||
|
img_i += 1
|
||||||
|
except Exception as e:
|
||||||
|
if str(e) == 'Document feeder out of documents':
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
print(e)
|
||||||
|
# snap_device.close()
|
||||||
|
# device_id = settings['device_id']
|
||||||
|
# snap_device = sane.open(device_id)
|
||||||
|
break
|
||||||
|
|
||||||
|
# TODO: maybe `adf_mode` can be set to `simplex`? see `--adf-mode` in http://sane-project.org/man/sane-epsonds.5.html
|
||||||
|
|
||||||
|
# for img in snap_device.multi_scan():
|
||||||
|
# if not isinstance(img, Image.Image):
|
||||||
|
# break
|
||||||
|
# settings['dpage_number'] = dpage_number + direction * img_i
|
||||||
|
# store_image(img, settings)
|
||||||
|
# img_i += 1
|
||||||
|
|
||||||
|
|
||||||
|
def scan_adf_duplex(snap_device, settings):
|
||||||
|
"""
|
||||||
|
Scan pages from source 'ADF Duplex' (a DADF scanner).
|
||||||
|
"""
|
||||||
|
set_scan_area(settings)
|
||||||
|
img_i = 0
|
||||||
|
dpage_number = settings.get('dpage_number', 1)
|
||||||
|
side = 'b'
|
||||||
|
for img in snap_device.multi_scan():
|
||||||
|
if not isinstance(img, Image.Image):
|
||||||
|
continue
|
||||||
|
side = 'a' if side == 'b' else 'b'
|
||||||
|
settings['target'] = side
|
||||||
|
settings['dpage_number'] = dpage_number + img_i // 2
|
||||||
|
store_image(img, settings)
|
||||||
|
img_i += 1
|
||||||
|
|
||||||
|
|
||||||
|
def set_scan_area(settings):
|
||||||
|
"""
|
||||||
|
Set coordinates of the scan area, using device params and paper size.
|
||||||
|
"""
|
||||||
|
device_id = settings.get('device_id')
|
||||||
|
device_settings = settings.get('device_settings', {}).get(device_id, {})
|
||||||
|
paper_size = device_settings.get('paper_size', '')
|
||||||
|
if paper_size.startswith('DIN A5'):
|
||||||
|
paper_width_mm = 148
|
||||||
|
paper_height_mm = 210
|
||||||
|
elif paper_size.startswith('Letter'):
|
||||||
|
paper_width_mm = 216
|
||||||
|
paper_height_mm = 279
|
||||||
|
elif paper_size.startswith('115x158mm'):
|
||||||
|
paper_width_mm = 115
|
||||||
|
paper_height_mm = 158
|
||||||
|
elif paper_size.startswith('145x420mm'):
|
||||||
|
paper_width_mm = 145
|
||||||
|
paper_height_mm = 420
|
||||||
|
elif paper_size.startswith('157x240mm'):
|
||||||
|
paper_width_mm = 157
|
||||||
|
paper_height_mm = 240
|
||||||
|
elif paper_size.startswith('170x240mm'):
|
||||||
|
paper_width_mm = 170
|
||||||
|
paper_height_mm = 240
|
||||||
|
elif paper_size.startswith('210x440mm'):
|
||||||
|
paper_width_mm = 210
|
||||||
|
paper_height_mm = 440
|
||||||
|
else:
|
||||||
|
paper_width_mm = 210
|
||||||
|
paper_height_mm = 297
|
||||||
|
scan_width_mm = snap_device['tl_x'].constraint[1]
|
||||||
|
if scan_width_mm > paper_width_mm:
|
||||||
|
if 'centered' in paper_size:
|
||||||
|
offset_hl = offset_hr = (scan_width_mm - paper_width_mm) / 2
|
||||||
|
else:
|
||||||
|
offset_hl = 0
|
||||||
|
offset_hr = scan_width_mm - paper_width_mm
|
||||||
|
snap_device.tl_x = offset_hl
|
||||||
|
snap_device.br_x = scan_width_mm - offset_hr
|
||||||
|
snap_device.tl_y = 0
|
||||||
|
snap_device.br_y = paper_height_mm
|
||||||
|
#print(paper_size, snap_device.tl_x,snap_device.tl_x,snap_device.br_x,snap_device.br_y)
|
||||||
|
|
||||||
|
|
||||||
|
def store_image(img, settings):
|
||||||
|
dpage_number = settings.get('dpage_number', 1)
|
||||||
|
target = settings.get('target', 'a')
|
||||||
|
img_name = f'{dpage_number:04d}{target}.png'
|
||||||
|
img.save(archive_dir / settings.get('collection_choice', '.') / img_name)
|
||||||
|
#img_small = img.resize((thumbnail_width, thumbnail_height), Image.BICUBIC)
|
||||||
|
# device_id = settings['device_id']
|
||||||
|
# device_settings = settings['device_settings'][device_id]
|
||||||
|
# mode = device_settings.get('mode', '?')
|
||||||
|
# resolution = device_settings.get('resolution', '?')
|
||||||
|
#collection_name = device_settings.get('collection_name', '').replace(' ', '_')
|
||||||
|
#img_small_name = f'{dpage_number:04d}{target}_{mode}_{resolution}_{collection_name}.png'
|
||||||
|
#img_small.save(cache_dir_small / img_small_name)
|
||||||
|
update_settings({'dpage_number': dpage_number})
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print('Please give the archive basedir as argument 1.')
|
||||||
|
sys.exit(2)
|
||||||
|
init(sys.argv[1])
|
||||||
|
web.run_app(app, port=8066)
|
7
static/bootstrap/css/bootstrap.min.css
vendored
Normal file
7
static/bootstrap/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
static/bootstrap/css/bootstrap.min.css.map
Normal file
1
static/bootstrap/css/bootstrap.min.css.map
Normal file
File diff suppressed because one or more lines are too long
7
static/bootstrap/js/bootstrap.min.js
vendored
Normal file
7
static/bootstrap/js/bootstrap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
static/bootstrap/js/bootstrap.min.js.map
Normal file
1
static/bootstrap/js/bootstrap.min.js.map
Normal file
File diff suppressed because one or more lines are too long
240
static/css/base.css
Normal file
240
static/css/base.css
Normal file
|
@ -0,0 +1,240 @@
|
||||||
|
/* X-Small devices (portrait phones, less than 576px) */
|
||||||
|
@media screen and (max-width:575.98px) {
|
||||||
|
#logo {
|
||||||
|
min-width: 180px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Small devices (landscape phones, less than 768px) */
|
||||||
|
@media screen and (max-width: 767.98px) and (min-width:576px) {
|
||||||
|
#logo {
|
||||||
|
min-width: 210px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Medium devices (tablets, less than 992px) */
|
||||||
|
@media screen and (max-width: 991.98px) and (min-width:768px) {
|
||||||
|
#logo {
|
||||||
|
min-width: 240px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Large devices (desktops, less than 1200px) */
|
||||||
|
@media screen and (max-width: 1199.98px) and (min-width:992px) {
|
||||||
|
#logo {
|
||||||
|
min-width: 270px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* X-Large devices (large desktops, less than 1400px) */
|
||||||
|
@media screen and (max-width: 1399.98px) and (min-width:1200px) {
|
||||||
|
#logo {
|
||||||
|
min-width: 300px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* XX-Large devices (large desktops, more than 1400px) */
|
||||||
|
@media screen and (min-width:1400px) {
|
||||||
|
#logo {
|
||||||
|
min-width: 320px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
.noprint {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
min-width: 100vw;
|
||||||
|
min-height: 100vh;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#footer {
|
||||||
|
background-color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
a, a:link, a:visited, a:hover, a:active {
|
||||||
|
color: #006699;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,h2,h3,h4,h5,h6 {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 0;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.btns > li {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.arrow {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0 0 10px 20px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.arrow > li {
|
||||||
|
padding: 5px 0 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.arrow > li:before {
|
||||||
|
content: "➜";
|
||||||
|
position: absolute;
|
||||||
|
margin-left: -24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.bool {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.bool > li {
|
||||||
|
padding-top: 5px;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.bool > li.success:before {
|
||||||
|
content: "☑";
|
||||||
|
position: absolute;
|
||||||
|
margin-left: -24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.bool > li.failure:before {
|
||||||
|
content: "☐";
|
||||||
|
position: absolute;
|
||||||
|
margin-left: -24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.errorlist {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
#form-login {
|
||||||
|
max-width: 340px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.menu {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "fa";
|
||||||
|
src: url("../fonts/fa-regular-400.woff2") format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
.fatt {
|
||||||
|
font-family: fa;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
details > summary {
|
||||||
|
color: #006699;
|
||||||
|
}
|
||||||
|
|
||||||
|
meter {
|
||||||
|
background: lightgrey;
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
meter.meter-narrow {
|
||||||
|
background: lightgrey;
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
th.number, td.number {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
th.rotate {
|
||||||
|
height: 140px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
th.rotate > div {
|
||||||
|
transform:
|
||||||
|
translate(25px, -5px)
|
||||||
|
rotate(315deg);
|
||||||
|
width: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
th.rotate > div > span {
|
||||||
|
border-bottom: 2px dotted #000;
|
||||||
|
padding: 5px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.date {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
form > table > tbody > tr > th {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
select.fgrow2 {
|
||||||
|
flex-grow: 2 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.input-group-text {
|
||||||
|
padding-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.input-right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
input#id_password, input#id_username {
|
||||||
|
max-width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cursor-cell {
|
||||||
|
cursor: cell;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline-info, .btn-outline-info:active, .btn-outline-info:focus, .btn-outline-info:visited {
|
||||||
|
border-color: #9a1662 !important;
|
||||||
|
color: #9a1662 !important;
|
||||||
|
background-color: white !important;
|
||||||
|
}
|
||||||
|
.btn-outline-info:hover {
|
||||||
|
border-color: #9a1662 !important;
|
||||||
|
color: white !important;
|
||||||
|
background-color: #9a1662 !important;
|
||||||
|
}
|
||||||
|
.btn-outline-info:focus {
|
||||||
|
box-shadow: rgba(154, 22, 98, 0.6) 0 0 0 3px;
|
||||||
|
color: white !important;
|
||||||
|
background-color: #9a1662 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.snap-img {
|
||||||
|
background: #FFF;
|
||||||
|
width: 181px;
|
||||||
|
height: 256px;
|
||||||
|
display: block;
|
||||||
|
margin: 10px auto;
|
||||||
|
border: 1px solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.snaps {
|
||||||
|
height: calc(100vh - 90px);
|
||||||
|
scroll-snap-align: end;
|
||||||
|
scroll-snap-type: y mandatory;
|
||||||
|
scrollbar-color: #dd4444 white;
|
||||||
|
scrollbar-width: auto;
|
||||||
|
}
|
7
static/jquery-ui/jquery-ui.min.css
vendored
Normal file
7
static/jquery-ui/jquery-ui.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
6
static/jquery-ui/jquery-ui.min.js
vendored
Normal file
6
static/jquery-ui/jquery-ui.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2
static/jquery/jquery.min.js
vendored
Normal file
2
static/jquery/jquery.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
static/jquery/jquery.min.js.map
Normal file
1
static/jquery/jquery.min.js.map
Normal file
File diff suppressed because one or more lines are too long
6
static/popper/popper.min.js
vendored
Normal file
6
static/popper/popper.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
static/popper/popper.min.js.map
Normal file
1
static/popper/popper.min.js.map
Normal file
File diff suppressed because one or more lines are too long
169
templates/rsnaps.html
Normal file
169
templates/rsnaps.html
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>{% block title %}rsnaps{% endblock %}</title>
|
||||||
|
<meta name="generator" content="aiohttp" />
|
||||||
|
<meta name="title" content="rsnaps" />
|
||||||
|
<meta name="description" content="Capture tool for scans and photos." />
|
||||||
|
<meta name="revisit-after" content="3600" />
|
||||||
|
<meta name="language" content="EN" />
|
||||||
|
<meta name="robots" content="noindex" />
|
||||||
|
<meta http-equiv="x-dns-prefetch-control" content="off" />
|
||||||
|
<link href="/static/bootstrap/css/bootstrap.min.css" rel="stylesheet" type="text/css" />
|
||||||
|
<link href="/static/css/base.css" rel="stylesheet" type="text/css" />
|
||||||
|
{% block stylesheets %}{% endblock %}
|
||||||
|
{% block head %}{% endblock %}
|
||||||
|
</head>
|
||||||
|
<body class="d-flex bg-white body">
|
||||||
|
{% block page %}
|
||||||
|
<div id="page" class="d-flex flex-row page">
|
||||||
|
<div class="d-flex flex-column justify-content-start bg-light border-right border-dark">
|
||||||
|
<a href="/">
|
||||||
|
<header class="d-flex flex-row bg-dark p-2">
|
||||||
|
<span class="text-white "><h1 class="text-info fw-bold">rsnaps</h1></span>
|
||||||
|
</header>
|
||||||
|
</a>
|
||||||
|
<div class="d-flex flex-row">
|
||||||
|
<form id="settings" method="POST" action="/detect">
|
||||||
|
<div class="p-2 pe-0">
|
||||||
|
<button class="btn btn-sm btn-secondary" type="submit"><strong>Detect devices</strong></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<form id="device" method="POST" action="/device">
|
||||||
|
<div class="p-2">
|
||||||
|
<select id="device" name="device_id" class="form-select form-select-sm w-auto" aria-label="Choose device" onchange="submit()">
|
||||||
|
<option value="">–Device–</option>
|
||||||
|
{% for device in devices %}
|
||||||
|
<option value="{{ device[0] }}"{% if device[0] == device_id %} selected{% endif %}>{{ device[2] }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<form id="collection_choice" method="POST" action="/collection">
|
||||||
|
<div class="d-flex flex-row p-2 pt-0">
|
||||||
|
<select id="collection_choice" name="collection_choice" class="form-select form-select-sm" aria-label="Choose collection" onchange="submit()">
|
||||||
|
{% for coll in collection_choices %}
|
||||||
|
<option value="{{ coll }}"{% if collection_choice == coll %} selected{% endif %}>{{ coll }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<form id="collection_delete" method="POST" action="/collection/delete">
|
||||||
|
<div class="d-flex flex-row p-2 pt-0">
|
||||||
|
<input type="hidden" name="collection_choice" value="{{ collection_choice }}">
|
||||||
|
<button class="btn btn-sm btn-danger" type="submit"><strong>DELETE</strong></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% if device_id %}
|
||||||
|
<form id="snap" method="POST" action="/snap">
|
||||||
|
<input type="hidden" name="device_id" value="{{ device_id }}">
|
||||||
|
<div class="ms-2 me-2 mb-2">
|
||||||
|
<span><strong>Settings</strong> for next snaps ⓘ</span>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body p-2 pe-2">
|
||||||
|
<div class="mt-0">
|
||||||
|
<span data-bs-toggle="tooltip" data-bs-placement="right" title="Name of collection"><strong>Start new collection</strong> ⓘ</span><br>
|
||||||
|
<input class="form-control w-100" id="collection_new" name="collection_new" type="text" value="{{ collection_new }}" placeholder="Path/to/Coll A/part 1">
|
||||||
|
</div>
|
||||||
|
<!-- <div class="mt-2">
|
||||||
|
<span data-bs-toggle="tooltip" data-bs-placement="right" title="Free annotation text (optional, short)"><strong>Collection description</strong> ⓘ</span><br>
|
||||||
|
<input class="form-control w-auto" id="collection_description" name="collection_description" type="text" value="{{ collection_description }}" placeholder="short description of collection">
|
||||||
|
</div>
|
||||||
|
-->
|
||||||
|
<div class="d-flex flex-row p-2 ps-0 pe-0">
|
||||||
|
<select id="paper_size" name="paper_size" class="form-select form-select-sm w-100" aria-label="Paper size">
|
||||||
|
{% for p_s in paper_sizes %}
|
||||||
|
<option value="{{ p_s }}"{% if p_s == paper_size %} selected{% endif %}>{{ p_s }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-row">
|
||||||
|
<div>
|
||||||
|
<span data-bs-toggle="tooltip" data-bs-placement="right" title="For colored originals use Color"><strong>Mode</strong> ⓘ</span><br>
|
||||||
|
{% for mode_ in modes %}
|
||||||
|
<input class="form-check-input" type="radio" name="mode" id="mode_{{ mode_ }}" value="{{ mode_ }}"{% if mode == mode_ %} checked{% endif %}>
|
||||||
|
<label class="form-check-label" for="mode_{{ mode_ }}">{{ mode_ }}</label><br>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<div class="ps-4">
|
||||||
|
<span data-bs-toggle="tooltip" data-bs-placement="right" title="Image resolution in dpi"><strong>Resolution</strong> ⓘ</span><br>
|
||||||
|
{% for resolution_ in resolutions %}
|
||||||
|
<input class="form-check-input" type="radio" name="resolution" id="resolution_{{ resolution_ }}" value="{{ resolution_ }}"{% if resolution == resolution_ %} checked{% endif %}>
|
||||||
|
<label class="form-check-label" for="resolution_{{ resolution_ }}">{{ resolution_ }}</label><br>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2">
|
||||||
|
<span data-bs-toggle="tooltip" data-bs-placement="right" title="Use this page number for the next snap (e.g. 32 for page 0032x.png)"><strong>Next (duplex) page number</strong> ⓘ</span><br>
|
||||||
|
<input class="form-control w-auto" id="dpage_number" name="dpage_number" type="text" value="{{ dpage_number }}" placeholder="use next" size="4">
|
||||||
|
</div>
|
||||||
|
<div class="mt-2">
|
||||||
|
<span data-bs-toggle="tooltip" data-bs-placement="right" title="File name target"><strong>Target</strong> (if not duplex) ⓘ</span><br>
|
||||||
|
<input class="form-check-input" type="radio" name="target" id="target_a" value="a"{% if target == 'a' %} checked{% endif %}>
|
||||||
|
<label class="form-check-label" for="target_a"><strong>a</strong> (front pages)<br>increase page number by 1</label><br>
|
||||||
|
<input class="form-check-input" type="radio" name="target" id="target_b" value="b"{% if target == 'b' %} checked{% endif %}>
|
||||||
|
<label class="form-check-label" for="target_b"><strong>b</strong> (back pages)<br>decrease page number by 1</label>
|
||||||
|
</div>
|
||||||
|
<div>Existing images will be overwritten!</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-grow-1 flex-column">
|
||||||
|
<div class="d-flex flex-column flex-wrap p-2 bg-white">
|
||||||
|
<div class="snaps overflow-auto">
|
||||||
|
<div class="d-flex flex-wrap">
|
||||||
|
{% for name in images %}
|
||||||
|
<div class"snap-img">
|
||||||
|
<a href="/image/{{ name }}"><img class="snap-img m-2 mb-0" src="/image-small/{{ name }}"></a>
|
||||||
|
<div class="d-flex ps-2 pe-2"><small><strong>{{ name }}</strong>
|
||||||
|
{% if mode == 'Color' %}<span class="bg-danger"> </span> <span class="bg-success"> </span> <span class="bg-primary"> </span>{% else %}<span class="bg-dark"> </span> <span class="bg-secondary"> </span> <span class="bg-white"> </span>{% endif %}
|
||||||
|
<strong>{{ resolution }}</strong>dpi</small><div class="ms-auto"><form id="image_delete_{{ name }}" method="POST" action="/image-delete/{{ name }}"><button type="submit" class="form-control btn btn-sm p-0 m-0">❌</button></form></div></div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-row justify-content-between bg-light h-100 border-top border-dark">
|
||||||
|
{% if device_id %}
|
||||||
|
{% for source_ in sources %}
|
||||||
|
<button form="snap" class="btn btn-success me-1 rounded-0" type="submit" name="snap" value="{{ source_ }}"><span class="fs-2 fw-bold p-0 m-0">{% if source_ == 'Automatic Document Feeder' %}ADF{% else %}{{ source_ }}{% endif %}</span></button>
|
||||||
|
{% endfor %}
|
||||||
|
<div id="page-ops" class="d-flex flex-row">
|
||||||
|
<div class="d-flex flex-column justify-content-center">
|
||||||
|
<div id="page-ops-input" class="d-flex flex-row justify-content-between">
|
||||||
|
<input form="pages" class="form-control w-auto h-auto" type="text" name="pages" placeholder="pages, e.g. 5a-10a,12,14-16">
|
||||||
|
<div class="align-self-center ps-2 pe-2">
|
||||||
|
<input form="pages" class="form-check-input" type="checkbox" id="lossy" name="lossy" value="lossy">
|
||||||
|
<label class="form-check-label" for="lossy"> Lossy</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="page-ops-buttons" class="d-flex flex-row justify-content-between">
|
||||||
|
<button form="pages" class="btn btn-secondary btn-sm" type="submit" name="operation" value="rotate180"><big><strong>Rotate pages 180°</strong></big></button>
|
||||||
|
<button form="pages" class="btn btn-danger btn-sm ms-1" type="submit" name="operation" value="delete"><big><strong>Delete pages</strong></big></button>
|
||||||
|
<button form="pages" class="btn btn-secondary btn-sm ms-1" type="submit" name="operation" value="pdf"><big><strong>Create PDF</strong></big></button>
|
||||||
|
<button form="pages" class="btn btn-secondary btn-sm ms-1" type="submit" name="operation" value="pdf_ocr"><big><strong>PDF+OCR</strong></big></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
<form id="pages" method="POST" action="/pages-operation"></form>
|
||||||
|
<script src="/static/jquery/jquery.min.js"></script>
|
||||||
|
<script src="/static/popper/popper.min.js"></script>
|
||||||
|
<script src="/static/bootstrap/js/bootstrap.min.js"></script>
|
||||||
|
<script>
|
||||||
|
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
|
||||||
|
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||||
|
return new bootstrap.Tooltip(tooltipTriggerEl)
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% block scripts %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in a new issue