Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42713b77d7 | ||
|
|
690f88119d | ||
|
|
78f391536b | ||
|
|
7bdd1828a9 | ||
|
|
a8f513eeeb | ||
|
|
af18bc0684 | ||
|
|
b621df6947 | ||
|
|
313c9e7dc1 | ||
|
|
9d04795f7f | ||
|
|
9a08e71e7f | ||
|
|
790d3022f6 | ||
|
|
ec311af796 | ||
|
|
c725bf79da | ||
|
|
9559f76fae | ||
|
|
45736b7c2b | ||
|
|
5629e960b9 | ||
|
|
79fd8d01a5 | ||
|
|
79fe7a0a85 | ||
|
|
b4b32a35b5 | ||
|
|
4634b3db55 | ||
|
|
f5053158d4 | ||
|
|
dfa4ce1612 | ||
|
|
585595a98e | ||
|
|
f6396fbaac | ||
|
|
3859bae85e | ||
|
|
ee1a7baae7 | ||
|
|
a4da05b66b | ||
|
|
4d67812d51 | ||
|
|
3534742ef9 | ||
|
|
8bfd46c80d | ||
|
|
cc6e9cecc0 | ||
|
|
208657f840 | ||
|
|
f3de980447 | ||
|
|
eb8992e58b | ||
|
|
72ad618ae6 | ||
|
|
f07d0c39bb | ||
|
|
9c5c7d9be0 | ||
|
|
0b19b084e2 | ||
|
|
9b4516af7a | ||
|
|
1eb45de5c9 | ||
|
|
390b9924f5 | ||
|
|
c28858a099 | ||
|
|
f00b3c00cd | ||
|
|
4e4f0bfa1f | ||
|
|
0a31acf888 | ||
|
|
b91096c615 | ||
|
|
95d9e8d91a | ||
|
|
cb6c1939e9 | ||
|
|
3764ee872a | ||
|
|
e402d5cb4b | ||
|
|
53cd04799a | ||
|
|
f2545d4496 | ||
|
|
9b81e76ed4 | ||
|
|
0956fc81aa | ||
|
|
72279e7759 | ||
|
|
6f9b948064 | ||
|
|
4eca0a165b | ||
|
|
067e61e03a | ||
|
|
1b46481f7e | ||
|
|
d8d9c41abb | ||
|
|
a8cad72f72 | ||
|
|
86c04305f4 | ||
|
|
0a110fac55 | ||
|
|
f8970ad862 | ||
|
|
fcfc78b7ee | ||
|
|
8bb244df24 | ||
|
|
8a1cb70479 | ||
|
|
2c579700d6 | ||
|
|
87ff6c8301 | ||
|
|
5915259bee | ||
|
|
969e54f0e3 | ||
|
|
b923612323 | ||
|
|
dc2b161306 | ||
|
|
ae49e3b6db | ||
|
|
0fc5067ab6 | ||
|
|
5eb5b5ba73 | ||
|
|
aa10a70d70 | ||
|
|
f99fd686de | ||
|
|
22dd9314ea | ||
|
|
73b8b88724 | ||
|
|
4863a8e521 | ||
|
|
2366629774 | ||
|
|
814ad36e51 | ||
|
|
1c2adc3d89 | ||
|
|
37923ffe52 | ||
|
|
773e28478c | ||
|
|
e6db071273 | ||
|
|
de74b80335 | ||
|
|
5cba68b93d | ||
|
|
d2d39de92f | ||
|
|
0b834411fe | ||
|
|
c935ba070b | ||
|
|
e30fffa8a4 | ||
|
|
e760be9e19 | ||
|
|
38280e77f8 | ||
|
|
6b9b5cc5d5 | ||
|
|
5f01c5e330 | ||
|
|
5d08303805 | ||
|
|
7965b1f930 | ||
|
|
0a1216bf14 | ||
|
|
f10a0f7707 | ||
|
|
dc11802809 | ||
|
|
4cce0077d0 | ||
|
|
d293e05946 | ||
|
|
4030258bbc | ||
|
|
db388165a9 | ||
|
|
3d6907f7f6 | ||
|
|
701c3b371b | ||
|
|
684e5b4944 | ||
|
|
a964080f77 | ||
|
|
3f72f16958 | ||
|
|
b4f2582766 | ||
|
|
c77cc7c837 | ||
|
|
f3715daf15 | ||
|
|
c87221a4e6 | ||
|
|
c409fa5825 | ||
|
|
09c485bd88 | ||
|
|
9d51a1b5ab | ||
|
|
43e7765efd | ||
|
|
352f009c77 | ||
|
|
399b5548ca | ||
|
|
7b1e5b4f41 | ||
|
|
33e0b16174 | ||
|
|
a613722e96 | ||
|
|
ad0126185f | ||
|
|
be45871d10 | ||
|
|
252221fd8b | ||
|
|
d25c49ba81 | ||
|
|
5112e9e857 | ||
|
|
757b72b0af | ||
|
|
75c5b92cb9 | ||
|
|
d673126994 | ||
|
|
d89a633ba7 | ||
|
|
710d797299 | ||
|
|
fc75254c60 | ||
|
|
f453e94f14 | ||
|
|
8f8aaa93ed | ||
|
|
a90b9e669f | ||
|
|
e4f69cc1d6 | ||
|
|
a5852ba199 | ||
|
|
051b9da991 | ||
|
|
139d9f9841 | ||
|
|
532d65a355 | ||
|
|
9de38afb13 | ||
|
|
913c939dc9 | ||
|
|
9db9a3d6ec | ||
|
|
8423bd549b | ||
|
|
336d274a54 | ||
|
|
906d77b389 | ||
|
|
9416e850ff | ||
|
|
fd3248869c | ||
|
|
2a09a668f6 | ||
|
|
e788dde607 | ||
|
|
173a80864d | ||
|
|
aa115a8be3 | ||
|
|
e0441c4aa1 | ||
|
|
a861c58da2 | ||
|
|
b1306bd7a8 | ||
|
|
3a6eb383dc | ||
|
|
6c942ecefd | ||
|
|
a25f8ecc62 | ||
|
|
e1f4813d94 | ||
|
|
af526f078d | ||
|
|
16438c1312 | ||
|
|
dd6cb7ce20 | ||
|
|
653e2e23df | ||
|
|
a2033698fa | ||
|
|
ec1d585d40 | ||
|
|
a4e1f8e1f3 | ||
|
|
2e155c31bf | ||
|
|
e09ae9c68a | ||
|
|
c5bf1dd90d | ||
|
|
e96770c5e4 | ||
|
|
f4f0f3c022 | ||
|
|
0a42934c08 | ||
|
|
d8f47768f9 | ||
|
|
c9594a4a5f | ||
|
|
873f915212 | ||
|
|
079c162a96 | ||
|
|
25c8c4656f | ||
|
|
ffcae9a1a0 | ||
|
|
6e71fe1186 | ||
|
|
0885799010 | ||
|
|
8ffc99f648 | ||
|
|
2261c51eff | ||
|
|
5c470778a3 | ||
|
|
4124889f36 | ||
|
|
a23c22b0e8 | ||
|
|
dd1f5f7215 | ||
|
|
5e2206bae7 | ||
|
|
079ee86d43 | ||
|
|
3692868004 | ||
|
|
064f935699 | ||
|
|
8770fff968 | ||
|
|
82de78b6b0 | ||
|
|
2a52c6dec2 | ||
|
|
2898879be7 | ||
|
|
18e613657c | ||
|
|
a48ca556c7 | ||
|
|
9cba738b48 | ||
|
|
bccf2f423f | ||
|
|
390fdf8c05 | ||
|
|
166de3086b | ||
|
|
206c675df6 | ||
|
|
6c8f9223e9 | ||
|
|
85c6a974ca | ||
|
|
dccdcfaa91 | ||
|
|
b1da09f141 | ||
|
|
42c84531e4 | ||
|
|
a9ad805347 | ||
|
|
16bda74974 | ||
|
|
d274d88929 | ||
|
|
327df5cbbc | ||
|
|
46d0632fe2 | ||
|
|
ef1e7a814e | ||
|
|
1084724937 | ||
|
|
ecb0109d79 | ||
|
|
386cabff00 | ||
|
|
3bd5054634 | ||
|
|
6a8dd65aa2 | ||
|
|
6083b4f0a7 | ||
|
|
1a3ce59476 | ||
|
|
c395436ba3 | ||
|
|
8d23d0b441 | ||
|
|
c6a2716cdb | ||
|
|
5545bae76f | ||
|
|
7bccb8c748 | ||
|
|
173c0d1274 | ||
|
|
6953f32465 | ||
|
|
26b4d9bb4b | ||
|
|
34e564cd7d | ||
|
|
504d5776d2 | ||
|
|
ee23976858 | ||
|
|
f559316881 | ||
|
|
084610c242 | ||
|
|
9ff627472b | ||
|
|
956310d1ec | ||
|
|
1a982da442 | ||
|
|
4879a1f0de | ||
|
|
ce66bcc9c8 | ||
|
|
1ebf3144af | ||
|
|
7a1cccbc4e | ||
|
|
ebacff1b39 | ||
|
|
c7c447be66 | ||
|
|
91aa175602 | ||
|
|
b267494e4a | ||
|
|
f687180ecc | ||
|
|
6f4b38b103 | ||
|
|
d32324859c | ||
|
|
48222b87b5 | ||
|
|
62e5edc72b | ||
|
|
2846d46bb8 | ||
|
|
47ef1914d4 | ||
|
|
df157552f3 | ||
|
|
0b3a526049 | ||
|
|
1e80d412fa | ||
|
|
df6e106203 | ||
|
|
bd0f005861 | ||
|
|
6ba4b7b3f3 | ||
|
|
2c11349ee8 | ||
|
|
b0afef09ef | ||
|
|
72fa347c38 | ||
|
|
96d68c2413 | ||
|
|
babc76fa74 | ||
|
|
dc06990e5d | ||
|
|
0ff0d2f8d1 | ||
|
|
81602cf420 | ||
|
|
607e2d7e81 | ||
|
|
b01d9e07e8 | ||
|
|
91db94cf2e | ||
|
|
416df803d4 | ||
|
|
037b96ca16 | ||
|
|
bb258fc99c | ||
|
|
4b8ccbe8cb | ||
|
|
ab1ff3331b | ||
|
|
3675ae918c | ||
|
|
0ba32b96b7 | ||
|
|
add64e4fa2 | ||
|
|
7fe2954ede | ||
|
|
ad202693b3 | ||
|
|
594ef83551 | ||
|
|
78b71618c1 | ||
|
|
b8aa89e1ec | ||
|
|
b4c1f66bc1 | ||
|
|
5172dbde8d | ||
|
|
d2908640c6 | ||
|
|
997bf7578d | ||
|
|
043258242c | ||
|
|
156d5d9a9c | ||
|
|
0b7e52fb5e | ||
|
|
a5feef07d0 | ||
|
|
f11bb53e61 | ||
|
|
68a57a7839 | ||
|
|
4194430dc1 | ||
|
|
a707c56fae | ||
|
|
3cba50bfbd | ||
|
|
ed5e17d0a4 | ||
|
|
ce0e0ecd4d | ||
|
|
7e1223c12c | ||
|
|
b83d7f6d1a | ||
|
|
80e957908a | ||
|
|
f0e7bea8ba | ||
|
|
0cdb9bd04a | ||
|
|
8224d89bc6 | ||
|
|
a2bbbe2a26 | ||
|
|
43f41863fa | ||
|
|
d71e50e83d | ||
|
|
1f598da3c1 | ||
|
|
d0cdbd5e1c | ||
|
|
5c56f61209 | ||
|
|
9bec85470a | ||
|
|
a03863a17d | ||
|
|
22cd9b2364 | ||
|
|
4fc7d6d93e | ||
|
|
71f0e7f545 | ||
|
|
895fddd85e | ||
|
|
5a59e4d543 | ||
|
|
b51abf2249 | ||
|
|
6d3f9ff15a | ||
|
|
5d1d1a712b | ||
|
|
6d5f8133e0 | ||
|
|
13018d3d5c | ||
|
|
14a85f9473 | ||
|
|
d22a1b3367 | ||
|
|
b913e5dfef | ||
|
|
dd8a5a4c72 | ||
|
|
36e9a54f02 | ||
|
|
3707af3b74 | ||
|
|
ced7ad9164 | ||
|
|
54bbbfdeb3 | ||
|
|
7f73a6ed1e | ||
|
|
dce206d3dc | ||
|
|
9304c856cf | ||
|
|
e5df98cbdf | ||
|
|
19bf3aeb00 | ||
|
|
e86be0031c | ||
|
|
6425977998 | ||
|
|
d57df2d980 | ||
|
|
664d0c7969 | ||
|
|
a354663ee1 | ||
|
|
b21b048ec4 | ||
|
|
709c65b41a | ||
|
|
67f99c5bb7 | ||
|
|
d55e673d9c | ||
|
|
21b90d2d14 | ||
|
|
2def7e3392 | ||
|
|
b0dcaa7512 | ||
|
|
e8285b1d10 | ||
|
|
5ba56adb53 | ||
|
|
ca735278e0 | ||
|
|
b5ccbfdf25 | ||
|
|
8c35d6e6e4 | ||
|
|
d1e0c81eda | ||
|
|
10c8e4f8b4 | ||
|
|
6be2242c21 | ||
|
|
204c9d6ae1 | ||
|
|
6eb393590b | ||
|
|
07c6654057 | ||
|
|
4e15eb8d14 | ||
|
|
8b01ab8ad2 | ||
|
|
e0a522ad50 | ||
|
|
a1a8788c5a | ||
|
|
cccdc178c3 | ||
|
|
4eacb3454f | ||
|
|
82b8b41e80 | ||
|
|
581c5020ab | ||
|
|
3ef8872a1e | ||
|
|
28eec73eed | ||
|
|
bfe4a5b329 | ||
|
|
29097837d6 | ||
|
|
a40361db3c | ||
|
|
8b29e3cbab | ||
|
|
b170be120b | ||
|
|
9a6cd95e5f | ||
|
|
d464d3122e | ||
|
|
1327ab37d4 | ||
|
|
67553fc5c6 | ||
|
|
306a903854 | ||
|
|
b93cf51c0f | ||
|
|
6b994221c6 | ||
|
|
8b5b02e0d8 | ||
|
|
624df9bb23 | ||
|
|
fa06ea3600 | ||
|
|
31994258fb | ||
|
|
1f15ecbca5 | ||
|
|
bcf5657e5c | ||
|
|
2ae028bf38 | ||
|
|
b51a5887e5 | ||
|
|
fc523e837c | ||
|
|
caeba76a61 | ||
|
|
cd35216f21 | ||
|
|
e6a7b58863 | ||
|
|
56184a762f | ||
|
|
07ab98f5af | ||
|
|
04fb1892b4 | ||
|
|
173ce2f215 | ||
|
|
9b641055e1 | ||
|
|
4fa28d7e74 | ||
|
|
bed74501fc | ||
|
|
8c90f7c972 | ||
|
|
aa0ec40102 | ||
|
|
12c567ee10 | ||
|
|
d39778ce3a | ||
|
|
e824cdbc4e | ||
|
|
1d91c09963 | ||
|
|
e821ca46d5 | ||
|
|
a29e4952fb | ||
|
|
4cc0dc6b4a | ||
|
|
7263702de9 | ||
|
|
0287d91874 | ||
|
|
436af55050 | ||
|
|
d6128e6937 | ||
|
|
642437e804 | ||
|
|
4ce802fdb2 | ||
|
|
a672422b0b | ||
|
|
addc2cbad0 | ||
|
|
5f45f77b4e | ||
|
|
93f9bffb37 | ||
|
|
44149ad319 | ||
|
|
d80d963cea | ||
|
|
4da33b8050 | ||
|
|
5cbbff8472 | ||
|
|
fa6e47c277 | ||
|
|
4ea9cffebd | ||
|
|
ae68edefc5 | ||
|
|
1558e068f1 | ||
|
|
d2a9c413f8 | ||
|
|
a510b21b20 | ||
|
|
e33ba07aa4 | ||
|
|
373f27832b | ||
|
|
b20a6e4c5d | ||
|
|
49734d5456 | ||
|
|
26a415c5dd | ||
|
|
60be64a5f1 | ||
|
|
190294634c | ||
|
|
1722cb579d | ||
|
|
dc42beb6a8 | ||
|
|
378f543619 | ||
|
|
62924ee280 | ||
|
|
86a73191b0 | ||
|
|
86875997b8 | ||
|
|
b939584c7a | ||
|
|
bbd174071d | ||
|
|
e5b6fe1317 | ||
|
|
f15d9049eb | ||
|
|
7630c93e5b | ||
|
|
638d68aa8a | ||
|
|
a92dde058a | ||
|
|
580f2ebb4b | ||
|
|
01cae7a584 | ||
|
|
66337813e6 | ||
|
|
eb5a211e72 | ||
|
|
5142933120 | ||
|
|
06ab114aa8 | ||
|
|
1257419465 | ||
|
|
30404f53f0 | ||
|
|
1ce8edbdfe | ||
|
|
d4b704a0ae | ||
|
|
2d64e1536d | ||
|
|
c8b581ac31 | ||
|
|
ad8dead7df | ||
|
|
c9bd87254e | ||
|
|
f4cb424451 | ||
|
|
fef14778d5 | ||
|
|
86ec63f215 | ||
|
|
5b10ec9d39 | ||
|
|
800c75c4e5 | ||
|
|
24d64b04c3 | ||
|
|
48e2750551 | ||
|
|
e182c5f63e | ||
|
|
06d52326db | ||
|
|
ebfe4f0d29 | ||
|
|
ad22977c84 | ||
|
|
6ac50646f0 | ||
|
|
24b6a4ad50 | ||
|
|
e802896d4d | ||
|
|
0b5a20e593 | ||
|
|
642998ead6 | ||
|
|
698aab4f75 | ||
|
|
34231ac667 | ||
|
|
ddedf7cd2e | ||
|
|
9d127d354c | ||
|
|
2d2a4894ab | ||
|
|
862861e3ca | ||
|
|
892db88f0e | ||
|
|
eeb44f78cc | ||
|
|
863835f660 | ||
|
|
393c5a9ea4 | ||
|
|
c6b9a49cbb | ||
|
|
17a4831745 | ||
|
|
7caf1e85ff | ||
|
|
f59a757e8b | ||
|
|
872bafad4b | ||
|
|
8599400445 | ||
|
|
b6eebadf05 | ||
|
|
a4e88eb8f0 | ||
|
|
f6257c2183 | ||
|
|
64891c2fc3 | ||
|
|
fe156db41d | ||
|
|
0f942fb714 | ||
|
|
be8ca589d4 | ||
|
|
3b6f6782f0 | ||
|
|
21c0e045cb | ||
|
|
ebbf68bd08 | ||
|
|
2059e916da | ||
|
|
c22f245606 | ||
|
|
7b9025f397 | ||
|
|
b109445215 | ||
|
|
fd1cd8e50a | ||
|
|
c6c70c2171 | ||
|
|
a9a473f2e5 | ||
|
|
6268e2faff | ||
|
|
ec3f506500 | ||
|
|
00daa51a73 | ||
|
|
e60f4d3f43 | ||
|
|
7460745f80 | ||
|
|
5e14d5b0dd | ||
|
|
d118132fa6 | ||
|
|
5f47aac36f | ||
|
|
c6b2fa8851 | ||
|
|
1b92f447c3 | ||
|
|
82e7eb91d2 | ||
|
|
4f4ad0fb76 | ||
|
|
1d0b8641a0 | ||
|
|
daca919775 | ||
|
|
1598f2f0e5 | ||
|
|
2b23f7ec73 | ||
|
|
6528234608 | ||
|
|
642ebc6098 | ||
|
|
74fdfeea3f | ||
|
|
3754185f56 | ||
|
|
df9f5157bd | ||
|
|
aa060db5bc | ||
|
|
d43212d30b | ||
|
|
a0f9ca3a30 | ||
|
|
0cefe886ec | ||
|
|
f656c00f41 | ||
|
|
03da34ee24 | ||
|
|
9bccff4f88 | ||
|
|
2bd586e093 | ||
|
|
9af94ac9b7 | ||
|
|
8174089c8b | ||
|
|
41eb54cc0a | ||
|
|
12a2f78c4d | ||
|
|
d372f1f7fa | ||
|
|
6f5b75bcd0 | ||
|
|
a2d3e0b53e | ||
|
|
7f67556995 | ||
|
|
db8c37e58c | ||
|
|
a87c81a64f | ||
|
|
4b986a5943 | ||
|
|
2fae9b655e | ||
|
|
2541f6cf89 | ||
|
|
33b68454f3 | ||
|
|
977665d2b6 | ||
|
|
fd7497f00d | ||
|
|
790ff58f67 | ||
|
|
4b98ce391b | ||
|
|
417dbd43f6 | ||
|
|
7a12908db9 | ||
|
|
9462f0a28f | ||
|
|
e760622a5c | ||
|
|
1b086f60a9 | ||
|
|
85cbf94a6e | ||
|
|
6f4286e1b1 | ||
|
|
39888ae8c9 | ||
|
|
dd361ecd05 | ||
|
|
32759c9025 | ||
|
|
59440448ee | ||
|
|
51b54893ce | ||
|
|
1f3665f614 | ||
|
|
75c34b873a | ||
|
|
fe4296c53b | ||
|
|
c85278b31d | ||
|
|
5dbc080fa0 | ||
|
|
e02f6c1e97 | ||
|
|
8c9a8fc85c | ||
|
|
23d558ad8c | ||
|
|
be107b4fed | ||
|
|
8d2535e327 | ||
|
|
5eb4fe0052 | ||
|
|
d8ff4485f8 | ||
|
|
82bce463ae | ||
|
|
016dfd420c | ||
|
|
b59e761a14 | ||
|
|
17cd655752 | ||
|
|
b840b16c82 | ||
|
|
8f5c95f0f4 | ||
|
|
168fc60774 | ||
|
|
c84d0f606d | ||
|
|
8b54ce338f | ||
|
|
18c4aa10bf | ||
|
|
991db17fde | ||
|
|
2c07515907 | ||
|
|
27a3b80376 | ||
|
|
8c381a0227 | ||
|
|
86145a8c76 | ||
|
|
7513f5425c | ||
|
|
af3c3c6466 | ||
|
|
db3e75e33e | ||
|
|
ce49fc26dd | ||
|
|
d0d0a98dca | ||
|
|
3834d1a0bf | ||
|
|
33e982b3fd | ||
|
|
43d650e78c | ||
|
|
b4c65c5781 | ||
|
|
d96867e6ab | ||
|
|
0a5108e704 | ||
|
|
94c52a6fa3 | ||
|
|
57771f06a3 | ||
|
|
58abb5785c | ||
|
|
509e75eaff | ||
|
|
0c50eedb2a | ||
|
|
4581027246 | ||
|
|
31b5f63f85 | ||
|
|
957fb1494e | ||
|
|
9e3e4f2687 | ||
|
|
2155bcacb4 | ||
|
|
346da95899 | ||
|
|
f4f7946a0c | ||
|
|
c2919f2e1c | ||
|
|
a63d624052 | ||
|
|
af91489376 | ||
|
|
d146d2b65c | ||
|
|
4ff4ed24a8 | ||
|
|
c38ff90081 | ||
|
|
4c029e973f | ||
|
|
21cf9029e8 | ||
|
|
4a640b8dcd | ||
|
|
9471bc8921 | ||
|
|
7fe06c64fc | ||
|
|
d13d70fd56 | ||
|
|
58ec56180a | ||
|
|
32a88f1bad | ||
|
|
99ef42940c | ||
|
|
c152710617 | ||
|
|
8de0f9b86f | ||
|
|
23bc3d3a29 | ||
|
|
8307832ce9 | ||
|
|
dd1cf567db | ||
|
|
2490be8490 | ||
|
|
85e6c6669a | ||
|
|
00498282f5 | ||
|
|
e4cc9fcba7 | ||
|
|
a4555b1dae | ||
|
|
f35a2303bb | ||
|
|
82142fe5ef | ||
|
|
9be533b5f4 | ||
|
|
99653fcd32 | ||
|
|
5442c97ed8 | ||
|
|
0165255bd9 | ||
|
|
378e4dae3b | ||
|
|
cdf5afa753 | ||
|
|
a2deee4920 | ||
|
|
1efa79cce2 | ||
|
|
b3b61c152c | ||
|
|
e429c3d729 | ||
|
|
8308b20096 | ||
|
|
8b41f60b6e | ||
|
|
d56f749017 | ||
|
|
9f31774aa9 | ||
|
|
7d55f6e01f | ||
|
|
c3bd2f296d | ||
|
|
e40c60d4d8 | ||
|
|
3960232ae0 | ||
|
|
5fbb3fc6ac | ||
|
|
0b1db8fccd | ||
|
|
0417610f9b | ||
|
|
43a23e3695 | ||
|
|
c4ca572afd | ||
|
|
e04e4565a9 | ||
|
|
2391fb0be0 | ||
|
|
c16f79d51b | ||
|
|
e3e888efde | ||
|
|
84f7e34ace | ||
|
|
32e2175891 | ||
|
|
975abfde9a | ||
|
|
d929ed76c2 | ||
|
|
4a27124eab | ||
|
|
683ffb84e8 | ||
|
|
2f2602357b | ||
|
|
a5f429f499 | ||
|
|
bdb7f92131 | ||
|
|
09f15ac4c0 | ||
|
|
4fdbf55c11 | ||
|
|
fe2b07652b | ||
|
|
f6d7aa6e33 | ||
|
|
a005d14f91 | ||
|
|
6f66232d44 | ||
|
|
b8a780d684 | ||
|
|
82f393dd09 | ||
|
|
4952af1604 | ||
|
|
bcf77375c0 | ||
|
|
3eab161771 | ||
|
|
b7f38e976b | ||
|
|
a6567f2ae4 | ||
|
|
e860c56b75 | ||
|
|
2e15d52895 | ||
|
|
ce97af5a79 | ||
|
|
3831c4cd4d | ||
|
|
61a2674317 | ||
|
|
9ad8cbf1f6 | ||
|
|
123fde174d | ||
|
|
fd991a2380 | ||
|
|
6f5d77d930 | ||
|
|
5169ac633b | ||
|
|
5b6ab1e003 | ||
|
|
8f984bf958 | ||
|
|
9c5f0d0ec6 | ||
|
|
32041c43e1 | ||
|
|
599028bebb | ||
|
|
6faa8f7221 | ||
|
|
a4dc5e365f | ||
|
|
e2a563cc76 | ||
|
|
1037d73efb | ||
|
|
aeb7b142a9 | ||
|
|
422ea9777e | ||
|
|
2f1c743227 | ||
|
|
96ee21aee9 | ||
|
|
4b759af6ff | ||
|
|
25d2b0cda4 | ||
|
|
16dd8b54a8 | ||
|
|
c4dc5269d2 | ||
|
|
c36e9950ae | ||
|
|
0c0d53b10f | ||
|
|
63de7e1677 | ||
|
|
b0e92760a2 | ||
|
|
054c0773a3 | ||
|
|
89aa78b724 | ||
|
|
708113a514 | ||
|
|
95ef5410c2 | ||
|
|
868b3b4abd | ||
|
|
045bdff95a | ||
|
|
d12b27ac1d | ||
|
|
e4e00de79f | ||
|
|
a53a3937c2 | ||
|
|
343424b4d2 | ||
|
|
c5edff2c2f | ||
|
|
8c5f8b8ddd | ||
|
|
39da931a56 | ||
|
|
9fe354359b | ||
|
|
facc4750bc | ||
|
|
437c235738 | ||
|
|
9559b0b186 | ||
|
|
91456e19a4 | ||
|
|
a2d89f67c4 | ||
|
|
f34130d193 | ||
|
|
c5571388e2 | ||
|
|
9af59c0d6d | ||
|
|
55ae838cb7 | ||
|
|
c434b97f55 | ||
|
|
607eee198d | ||
|
|
5e2a7f8a56 | ||
|
|
fd9550acda | ||
|
|
b354511ac9 | ||
|
|
7be293f628 | ||
|
|
65855dc14c | ||
|
|
cac4a8b9b6 | ||
|
|
17d97b354a | ||
|
|
1c1b60fa9f | ||
|
|
6b745d892f | ||
|
|
fbf271a3ec | ||
|
|
8077718804 | ||
|
|
66bda3420a | ||
|
|
f6510e2b15 | ||
|
|
51abd79136 | ||
|
|
5607429d9a | ||
|
|
b8b7ecfe7f | ||
|
|
d4abe88452 | ||
|
|
cb3cfaa055 | ||
|
|
9db01c7ff5 | ||
|
|
d0301813cc | ||
|
|
cff37bf681 | ||
|
|
66d04dd6e3 | ||
|
|
06a1f987d4 | ||
|
|
e51e21c6b6 | ||
|
|
c5fa72bd4e | ||
|
|
bf99587aa1 | ||
|
|
d249aef57d | ||
|
|
43ab7c88d7 | ||
|
|
ca9669742d | ||
|
|
8a1dddc3ee | ||
|
|
0cd424ffcb | ||
|
|
fde550f9a7 | ||
|
|
a3726e4ce3 | ||
|
|
4ab0a8ff35 | ||
|
|
37f6f72df3 | ||
|
|
3f92867ae6 | ||
|
|
e63503d64b | ||
|
|
17d20309c7 | ||
|
|
fe7c69ce95 | ||
|
|
9baccee8c5 | ||
|
|
d5bb9929f3 | ||
|
|
72d3ee3a87 | ||
|
|
17c419dfcb | ||
|
|
84cc49b14b | ||
|
|
b7f63bc93d | ||
|
|
ad9a3b5302 | ||
|
|
4e4bcaf243 | ||
|
|
11afe3507f | ||
|
|
7691ba8535 | ||
|
|
b787a369ee | ||
|
|
9fb8b267af | ||
|
|
0a08d6ce1f | ||
|
|
f517efe819 | ||
|
|
5f5421f23d | ||
|
|
703b6db95c | ||
|
|
000040d497 | ||
|
|
5bd6665b49 | ||
|
|
1c303afe21 | ||
|
|
11a5c80917 | ||
|
|
9b2ab92913 | ||
|
|
0c4b69ec5a | ||
|
|
45bea1c0e0 | ||
|
|
db914d4cd1 | ||
|
|
df4a8faecd | ||
|
|
1273e7aeda | ||
|
|
e13a673b1a | ||
|
|
979b0bcaed | ||
|
|
3438afaffe | ||
|
|
681fa039cc | ||
|
|
69e80f1545 | ||
|
|
983835cce4 | ||
|
|
6c23b137e2 | ||
|
|
d656b2b3f2 | ||
|
|
031b800aac | ||
|
|
05eb85ee77 | ||
|
|
4da5214ca9 | ||
|
|
1ee829dd59 | ||
|
|
99db5d91ae | ||
|
|
3a4490ee36 | ||
|
|
a492e3b472 | ||
|
|
c3719d3b72 | ||
|
|
ad48fc6415 | ||
|
|
7f8018ffde | ||
|
|
80651fe12c | ||
|
|
a58209e895 | ||
|
|
775b958c55 | ||
|
|
cdcdd16865 | ||
|
|
b332d76782 | ||
|
|
3660007fc8 | ||
|
|
b55d7e57af | ||
|
|
6e99e7b346 | ||
|
|
4d26867dee | ||
|
|
78e8bf9cbf | ||
|
|
de61530d4d | ||
|
|
c149f860b5 | ||
|
|
68c852acec | ||
|
|
a8565bac6e | ||
|
|
6e8b0c3194 | ||
|
|
ff860e8362 | ||
|
|
cf4b04c5d1 | ||
|
|
078bc2abe9 | ||
|
|
d7b7ca0574 | ||
|
|
17ac9d7a9a | ||
|
|
a2a197ce4c | ||
|
|
944d59e5ad | ||
|
|
1c3e90a892 | ||
|
|
c728836956 | ||
|
|
0d80fab339 | ||
|
|
a650caa599 | ||
|
|
c6caff90a1 | ||
|
|
671c88d3b5 | ||
|
|
b2cfaedf91 | ||
|
|
19ba3ae011 | ||
|
|
feff1e38bb | ||
|
|
c8d6ea6b10 | ||
|
|
b0d9775343 | ||
|
|
462bfb84fb | ||
|
|
11ef78a891 | ||
|
|
638eb556ef | ||
|
|
fdefcd8af2 | ||
|
|
09457edad3 | ||
|
|
6460a7eb3e | ||
|
|
707ebeb151 | ||
|
|
e9bc093842 | ||
|
|
2eeaca1168 | ||
|
|
7755c5c5a7 | ||
|
|
793348a47c | ||
|
|
b241f66919 | ||
|
|
8ad034a678 | ||
|
|
a1a7b973e9 | ||
|
|
7bfcd0a9d5 | ||
|
|
f276c4ef1e | ||
|
|
77bbc22c50 | ||
|
|
a6805ed343 | ||
|
|
c4afc5c242 | ||
|
|
df32008667 | ||
|
|
db4598f76a | ||
|
|
ce13431ecf | ||
|
|
eb104b405d | ||
|
|
5f00e4f9d8 | ||
|
|
1a91cd4652 | ||
|
|
b0f1a55537 | ||
|
|
0c066d1d53 | ||
|
|
e4cfcec5f3 | ||
|
|
85c90404d7 | ||
|
|
4d011c28ea | ||
|
|
5304c631ec | ||
|
|
6fbeb6347d | ||
|
|
f83de20c37 | ||
|
|
423324af01 | ||
|
|
016a2a01d9 | ||
|
|
6189910c74 | ||
|
|
7117dc10de | ||
|
|
b601cb0cba | ||
|
|
0c781faf89 | ||
|
|
12769b96e5 | ||
|
|
a7b4ed9688 | ||
|
|
ee92ce8717 | ||
|
|
cbeddab35f | ||
|
|
98050534d6 | ||
|
|
9850231e91 | ||
|
|
fd810239b5 | ||
|
|
2cff6ad2d1 | ||
|
|
3ee306184b | ||
|
|
1cc4c45b7e | ||
|
|
a86cb8148a | ||
|
|
eeae6f8292 | ||
|
|
4dab299619 | ||
|
|
340e2bbac6 | ||
|
|
187283192b | ||
|
|
f855bdd36b | ||
|
|
9873d51f58 | ||
|
|
11a57c7a17 | ||
|
|
8aa678859d | ||
|
|
1beb7dfd37 | ||
|
|
9b60d3e285 | ||
|
|
3331a686fa | ||
|
|
c32ea3b374 | ||
|
|
c357d4146e | ||
|
|
f47cb2fade | ||
|
|
9c4b1aeb8d | ||
|
|
51ed381bfc | ||
|
|
5ee45411c9 | ||
|
|
16990890d8 | ||
|
|
cfb11559d5 | ||
|
|
8b8de7cc1d | ||
|
|
aba293fd80 | ||
|
|
066a293462 | ||
|
|
0bbd6885e2 | ||
|
|
5dd10c961c | ||
|
|
81fc95556c | ||
|
|
20ad032977 | ||
|
|
93f1b73579 | ||
|
|
fd427a8ec1 | ||
|
|
9444cf357b | ||
|
|
5ab69153ee | ||
|
|
eb5200d26a | ||
|
|
98a3fda1f5 | ||
|
|
e73740ae9d | ||
|
|
fb933edc0f | ||
|
|
ba41ccae1b | ||
|
|
df9e286e9c | ||
|
|
b9d6e46572 | ||
|
|
8347c0d662 | ||
|
|
45a361d112 | ||
|
|
522e1e948b | ||
|
|
8ed4e229f3 | ||
|
|
db29cae177 | ||
|
|
d5b6cbb95e | ||
|
|
396c39978a | ||
|
|
9d5f23e961 | ||
|
|
26a6232e1c | ||
|
|
7566d4b768 | ||
|
|
5c4c32ab3c | ||
|
|
692f7b3151 | ||
|
|
8d0958d7ea | ||
|
|
e9731b6bac | ||
|
|
0628a89041 | ||
|
|
c14f62752b | ||
|
|
24855045e1 | ||
|
|
ed236e0c27 | ||
|
|
db6aa22eae | ||
|
|
805aa776ad | ||
|
|
d0efdf643c | ||
|
|
22298b31be | ||
|
|
5cecb3ecb4 | ||
|
|
a139e64c67 | ||
|
|
db69b4d11a | ||
|
|
8bcb85720c | ||
|
|
09ca1bee97 | ||
|
|
23dd77ce0f | ||
|
|
32a076c039 | ||
|
|
e4baa8c0dd | ||
|
|
2fdaa76a0d | ||
|
|
7ee0c52a57 | ||
|
|
ac2fc9c2a0 | ||
|
|
188e08e98b | ||
|
|
ef1ef1cdf0 | ||
|
|
24da92d39e | ||
|
|
61afef549e | ||
|
|
cfd67ab6aa | ||
|
|
56067b590b | ||
|
|
4340ad9f12 | ||
|
|
70def4a0d0 | ||
|
|
8df1ea2754 | ||
|
|
e528adc603 | ||
|
|
c1af0fb18d | ||
|
|
0cb4e854e5 | ||
|
|
13ab23ba54 | ||
|
|
19263f00c6 | ||
|
|
50bd129d7a | ||
|
|
471cdea232 | ||
|
|
9d750828c7 | ||
|
|
5e025c3382 | ||
|
|
4410503349 | ||
|
|
486f73d5d6 | ||
|
|
b10285d11b | ||
|
|
c904b430b6 | ||
|
|
07d4fff3d4 | ||
|
|
7e388f59af | ||
|
|
84ec83151d | ||
|
|
0a72c12ff0 | ||
|
|
5adbddd1f9 | ||
|
|
c2fecffdb4 | ||
|
|
7dea0a8362 | ||
|
|
482cb788ed | ||
|
|
15a988b999 | ||
|
|
83398e54ea | ||
|
|
bcdd196699 | ||
|
|
0cd576e701 | ||
|
|
4d5e0eb749 | ||
|
|
7ed0f8f50e | ||
|
|
79c84eefa3 | ||
|
|
5398003160 | ||
|
|
58b2bed99d | ||
|
|
58c29ffb5c | ||
|
|
f615b6f0e8 | ||
|
|
e0c8dadcce | ||
|
|
9a86f53109 | ||
|
|
91cb092aa0 | ||
|
|
f4b87915df | ||
|
|
922a107b7f | ||
|
|
1c44fd4f3b | ||
|
|
c92ccc6134 | ||
|
|
1137534e97 | ||
|
|
39617dd739 | ||
|
|
6590875756 | ||
|
|
01bbf064e0 | ||
|
|
fc1c4f12f5 | ||
|
|
2647382cf6 | ||
|
|
783a128bd1 | ||
|
|
b214aa5b38 | ||
|
|
6e49bb3588 | ||
|
|
aa512b6181 | ||
|
|
a4667b5656 | ||
|
|
f65a3d3762 | ||
|
|
2fa43ecf09 | ||
|
|
427afc0616 | ||
|
|
9c7ee2bf23 | ||
|
|
c5cfaa950b | ||
|
|
4e2a98ead4 | ||
|
|
210f134b5b | ||
|
|
696c0721a0 | ||
|
|
aabab95418 | ||
|
|
7d614dd68b | ||
|
|
f57dda7939 | ||
|
|
1b4542aa77 | ||
|
|
6c7fca57ec | ||
|
|
486dc7e22c | ||
|
|
dc616bb507 | ||
|
|
902bda43e3 | ||
|
|
f7da63f68b | ||
|
|
5da26e4c9c | ||
|
|
c19c852705 | ||
|
|
a27ee3ee8c | ||
|
|
c2f316c2c5 | ||
|
|
974979b0a0 | ||
|
|
66586bdaab | ||
|
|
01d2ea309f | ||
|
|
e918480351 | ||
|
|
52fd84fa95 | ||
|
|
2c56b0935c | ||
|
|
4f69ace868 | ||
|
|
497c531112 | ||
|
|
b27b92fbf3 | ||
|
|
2e6ba2df8c | ||
|
|
67a405c6b7 | ||
|
|
58e6663806 | ||
|
|
602570fcf9 | ||
|
|
691f8ce254 | ||
|
|
22812e74b9 | ||
|
|
9d824e723d | ||
|
|
5dad800d85 | ||
|
|
56a56a4dcb | ||
|
|
3f1d9ef99c | ||
|
|
92c8a5885e | ||
|
|
7749d14252 | ||
|
|
9b92af5aed | ||
|
|
0bf26b03ae | ||
|
|
e2847ea4c3 | ||
|
|
19e35db2b7 | ||
|
|
df688742d5 | ||
|
|
42c2925f9d | ||
|
|
933f0b8f9b | ||
|
|
03ab5a8ee2 | ||
|
|
4f06920224 | ||
|
|
a733b09623 | ||
|
|
5483dacf52 | ||
|
|
ae7844ad88 | ||
|
|
a6e7485da6 | ||
|
|
3bcc6d6121 | ||
|
|
9fe067bbd9 | ||
|
|
f095e91cb4 | ||
|
|
66c8d4b47a | ||
|
|
721489a06c | ||
|
|
9a4493f211 | ||
|
|
edb4d6c586 | ||
|
|
b8cd3acd9e | ||
|
|
03779e33da | ||
|
|
c466483e82 | ||
|
|
e3a58219d1 | ||
|
|
72337094ca | ||
|
|
f472587d22 | ||
|
|
f34b3015b2 | ||
|
|
089ece2715 | ||
|
|
6438465e3f | ||
|
|
7d330afd81 | ||
|
|
68fbd9fcc9 | ||
|
|
c771938907 | ||
|
|
c2a947acf4 | ||
|
|
8c0009c5c8 | ||
|
|
cfc5cdf47d | ||
|
|
05152a8af9 | ||
|
|
0880b16491 | ||
|
|
06308a22ce | ||
|
|
80bd7de580 | ||
|
|
8b90c45437 | ||
|
|
72b920eb16 | ||
|
|
b4a51907d6 | ||
|
|
1ca1221432 | ||
|
|
40b72b0fa8 | ||
|
|
0e55b4ad52 | ||
|
|
7b4f5a8fc4 | ||
|
|
9261a38493 | ||
|
|
cc8ff318ed | ||
|
|
ad15e845f9 | ||
|
|
ab632f57cd | ||
|
|
13d20bd993 | ||
|
|
b973208137 | ||
|
|
942abf8074 | ||
|
|
ed9bb985e2 | ||
|
|
5a7a8e573b | ||
|
|
ce878db913 | ||
|
|
a3d58683b2 | ||
|
|
039e8ca7e7 | ||
|
|
0ebbd4e21b | ||
|
|
2cb75f6076 | ||
|
|
857d871364 | ||
|
|
632dab2cc0 | ||
|
|
7647918f2d | ||
|
|
75c5d8055c | ||
|
|
a938bbea55 | ||
|
|
414407fbd6 | ||
|
|
076fc717df | ||
|
|
2a04b2d82b | ||
|
|
065db414c0 | ||
|
|
19a054a78b | ||
|
|
9df24a81b7 | ||
|
|
40c0acd3f2 | ||
|
|
20db7f0a8f | ||
|
|
e54f6ee37f | ||
|
|
2da556bf79 | ||
|
|
b183ad8167 | ||
|
|
9e6b54c7ed | ||
|
|
d3b334c10f | ||
|
|
622f2c4bab | ||
|
|
07b638a394 | ||
|
|
9bee2405d8 | ||
|
|
8f040491bf | ||
|
|
8a18988706 | ||
|
|
47a954514b | ||
|
|
e3b65d4288 | ||
|
|
4704f7ed1d | ||
|
|
3a2745445a | ||
|
|
12e15bab15 | ||
|
|
9593aa4fb9 | ||
|
|
817d520e63 | ||
|
|
700abbb8a5 | ||
|
|
701ef1df3f | ||
|
|
0f5c484b62 | ||
|
|
cc7f2a3f02 | ||
|
|
755b5d87e3 | ||
|
|
e55a4115e1 | ||
|
|
16a6fd2ea9 | ||
|
|
e3fce112ed | ||
|
|
eacd26a68b | ||
|
|
0e88b3c38a | ||
|
|
a2170ef8d6 | ||
|
|
eed0424390 | ||
|
|
5ed05e08b1 | ||
|
|
58b26f6715 | ||
|
|
806daf4284 | ||
|
|
c64bc9329e | ||
|
|
dd01745519 | ||
|
|
501ce726e7 | ||
|
|
03076e89ce | ||
|
|
02f37293ee | ||
|
|
590942ad14 | ||
|
|
2ac028c759 | ||
|
|
2125b5bfab | ||
|
|
b96532caa4 | ||
|
|
995fc58466 | ||
|
|
c023cae299 | ||
|
|
237eaf9130 | ||
|
|
8b9ab25125 | ||
|
|
77e87abe8f | ||
|
|
3be02e1e8d | ||
|
|
64c9ede979 | ||
|
|
5b8d197812 | ||
|
|
2cba62dc4f | ||
|
|
288e28328f | ||
|
|
b8214b3c49 | ||
|
|
8681693994 | ||
|
|
1364c63b7c | ||
|
|
4ba9e8fe25 | ||
|
|
a195713bb4 | ||
|
|
600d31a907 | ||
|
|
be31cec332 | ||
|
|
22a7cd3421 | ||
|
|
8b61d2d521 | ||
|
|
559e5269d2 | ||
|
|
ebf6acb318 | ||
|
|
7acd75f013 | ||
|
|
93623b2226 | ||
|
|
d71fd089cb | ||
|
|
05aa43c856 | ||
|
|
de80fb6bc8 | ||
|
|
8e396f4be2 | ||
|
|
efec6da377 | ||
|
|
00ef53195e | ||
|
|
f564aaf485 | ||
|
|
5ac2d31d0d | ||
|
|
fda890ab47 | ||
|
|
a873278c2a | ||
|
|
e6d64be890 | ||
|
|
0e4d978d20 | ||
|
|
b12c2cfedf | ||
|
|
58cc70725e | ||
|
|
339afb02aa | ||
|
|
7ba0ff5c36 | ||
|
|
ff41fbf673 | ||
|
|
2435cd23ce | ||
|
|
a063cff720 | ||
|
|
0d396e1ac0 | ||
|
|
f5807a2053 | ||
|
|
eb4938a36f | ||
|
|
c5ad530bbf | ||
|
|
d11c428407 | ||
|
|
6182b1f53e | ||
|
|
00fc1a12e2 | ||
|
|
16af753206 | ||
|
|
fa48205bb8 | ||
|
|
f7dbf94071 | ||
|
|
b18e66e2ca | ||
|
|
7a5504dfa5 | ||
|
|
d1cad7bc68 | ||
|
|
c58d5c097c | ||
|
|
46157ca94e | ||
|
|
dd99511bcc | ||
|
|
5bc2efd3c7 | ||
|
|
1b18dbecf5 | ||
|
|
9f82c0eb6e | ||
|
|
68bac1b177 | ||
|
|
1495b78330 | ||
|
|
6f777d2848 | ||
|
|
5650eba848 | ||
|
|
5bc5dc93f3 | ||
|
|
c1e18bb825 | ||
|
|
58282ea0fb | ||
|
|
891da7834c | ||
|
|
5c229d48d5 | ||
|
|
53f660cf35 | ||
|
|
7b66ca68f2 | ||
|
|
ba71c3ffbd | ||
|
|
6707ad427a | ||
|
|
5b84549716 | ||
|
|
c74f2ee6e8 | ||
|
|
b32dd9f9d3 | ||
|
|
fb8b161f6c | ||
|
|
baddd6d233 | ||
|
|
6f554c6ae8 | ||
|
|
a71e4488b3 | ||
|
|
72156b5653 | ||
|
|
9fa471e053 | ||
|
|
31ef2fe907 | ||
|
|
9a8ec4b210 | ||
|
|
75aad4cc79 | ||
|
|
4b27feca98 | ||
|
|
45522cd15f | ||
|
|
677d9a4e76 | ||
|
|
efa7ea4fde | ||
|
|
137a6e45f5 | ||
|
|
29116e1dec | ||
|
|
87193335b9 | ||
|
|
cfd4f8a850 | ||
|
|
eaa324939f | ||
|
|
ef70e538f7 | ||
|
|
b7b912e56a | ||
|
|
4615cf2f1e | ||
|
|
eaf772f80a | ||
|
|
96ba75eabd | ||
|
|
fdfe52c1ad | ||
|
|
932b2e2a29 | ||
|
|
57e489c957 | ||
|
|
17a3fa671c | ||
|
|
2659afb4f6 | ||
|
|
7392115507 | ||
|
|
c54d0c7eaa | ||
|
|
b95eefc65f | ||
|
|
686207ab7f | ||
|
|
517b385fe5 | ||
|
|
795019b0c1 | ||
|
|
3127a73822 | ||
|
|
069ee6c91f | ||
|
|
3aac3a98ca | ||
|
|
268859a304 | ||
|
|
a96710aa7b | ||
|
|
edcc58826a | ||
|
|
7077c8220a | ||
|
|
f7cbf68edd | ||
|
|
68a58ee8a5 | ||
|
|
3109ec5091 | ||
|
|
e0599fe8d7 | ||
|
|
a749240589 | ||
|
|
6decdaa062 | ||
|
|
4d5c9b8cdf | ||
|
|
1e23ea5364 | ||
|
|
cf9a8a91b5 | ||
|
|
05d3a65e94 | ||
|
|
c043552f8b | ||
|
|
487ee2b6c9 | ||
|
|
8013fd50da | ||
|
|
4ec9ec12e3 | ||
|
|
ed96594727 | ||
|
|
91b7193249 | ||
|
|
008f65c6a9 | ||
|
|
7aa4e60af2 | ||
|
|
6ef26516d1 | ||
|
|
bb50501369 | ||
|
|
55eb481f30 | ||
|
|
4509ca6e75 | ||
|
|
c61fe9af61 | ||
|
|
4181a712d1 | ||
|
|
c171cb7286 | ||
|
|
1d09061130 | ||
|
|
a2203b2447 | ||
|
|
755f6e0979 | ||
|
|
95b2830d17 | ||
|
|
2669d8332e | ||
|
|
11fbd32e6e | ||
|
|
6a302fdb88 | ||
|
|
1d9cc239ee | ||
|
|
d240fc1ea6 | ||
|
|
e7d21dd826 | ||
|
|
e774b4650b | ||
|
|
8f8e6dcdd4 | ||
|
|
5252b88f0f | ||
|
|
ea69883386 | ||
|
|
eb343b1e37 | ||
|
|
9f02de55be | ||
|
|
7394a4cf49 | ||
|
|
ed9fb110b1 | ||
|
|
4650074428 | ||
|
|
70aa644c10 | ||
|
|
2ccb3edc58 | ||
|
|
a12a5f11ff | ||
|
|
f46e4067f4 | ||
|
|
47bf5585e8 | ||
|
|
72fb4d39be | ||
|
|
9681c0e05a | ||
|
|
2628575dab | ||
|
|
f7d59068e5 | ||
|
|
4cf10d62ca | ||
|
|
2f65e3aac8 | ||
|
|
e494cd7aa6 | ||
|
|
809880f46d | ||
|
|
5925ebe31c | ||
|
|
316709c291 | ||
|
|
0d6a2ba554 | ||
|
|
1fcb06b1aa | ||
|
|
36138c7c06 | ||
|
|
338593d9e7 | ||
|
|
d6eb1f9578 | ||
|
|
1cc9d2d3d1 | ||
|
|
58642aa98b | ||
|
|
7baaf00a38 | ||
|
|
5cc23dbf24 | ||
|
|
216d60ea2c | ||
|
|
8b0496d35e | ||
|
|
e44001641c | ||
|
|
47885f4230 | ||
|
|
921767e82e | ||
|
|
85f96b7fb0 | ||
|
|
890c7fd0f6 | ||
|
|
39c44bdd2f | ||
|
|
5f99f7f6ca | ||
|
|
4f864bce98 | ||
|
|
2974929b26 | ||
|
|
db837aa55c | ||
|
|
7200623007 | ||
|
|
73e02ae4ea | ||
|
|
d4cbef9457 | ||
|
|
ed8ff79e10 | ||
|
|
e725f64b6a | ||
|
|
0029cc4fe7 | ||
|
|
9637696a54 | ||
|
|
02b3ca6862 | ||
|
|
bc90f40a8f | ||
|
|
3d727ff4c0 | ||
|
|
b0eacd6586 | ||
|
|
7795701595 | ||
|
|
bf214eecb3 | ||
|
|
434b96d734 | ||
|
|
b9dc109892 | ||
|
|
1f40a70554 | ||
|
|
e14ffbf03f | ||
|
|
25a1dde57c | ||
|
|
bf96171b65 | ||
|
|
b7ff821fa3 | ||
|
|
b81daf71d1 | ||
|
|
faad1fc58a | ||
|
|
6f48181a56 | ||
|
|
f1305e5a37 | ||
|
|
f0e0f92776 | ||
|
|
807c8b0726 | ||
|
|
6333ec928c | ||
|
|
cd220d9ed9 | ||
|
|
76532649b8 | ||
|
|
b0dbaeafc5 | ||
|
|
2530d1791b | ||
|
|
94150f414a | ||
|
|
54e74f84cc | ||
|
|
76e7e8dbbb | ||
|
|
324598e992 | ||
|
|
9e765ddf46 | ||
|
|
6ac9e92f17 | ||
|
|
faaa4a1def | ||
|
|
0aa51f0f3a | ||
|
|
73431d9761 | ||
|
|
45cb4525cf | ||
|
|
8c84c515b6 | ||
|
|
1dfbbdebf4 | ||
|
|
740918daee | ||
|
|
1d10eac764 | ||
|
|
3f868118cd | ||
|
|
04d79b15b4 | ||
|
|
a13c398c06 | ||
|
|
e3b3f716ee | ||
|
|
cf43c06f46 | ||
|
|
74a5a18607 | ||
|
|
44241c6dd5 | ||
|
|
8fff496ffd | ||
|
|
edf75c519c | ||
|
|
9608b22d34 | ||
|
|
8ba4968c48 | ||
|
|
ffdd78f1a5 | ||
|
|
ad9f8ca78e | ||
|
|
78a686ecb4 | ||
|
|
59e786eb3c | ||
|
|
6d0461435f | ||
|
|
0a04a60f69 | ||
|
|
68d8642988 | ||
|
|
16f70ff054 | ||
|
|
c00aeafff0 | ||
|
|
83f35e00f3 | ||
|
|
786a2ad65a | ||
|
|
9425506c2a | ||
|
|
93b858afd1 | ||
|
|
7b0a3ec365 | ||
|
|
083d442529 | ||
|
|
b52eb95cf8 | ||
|
|
f4571e2508 | ||
|
|
b06ef03aac | ||
|
|
1d1962a106 | ||
|
|
4b98e9ff08 | ||
|
|
f83ca5d8ac | ||
|
|
95cb4d22d7 | ||
|
|
0c279b01a4 | ||
|
|
3b820ffa7b | ||
|
|
35cb416563 | ||
|
|
cdb737259c | ||
|
|
0843b5939c | ||
|
|
2b5f23a2d1 | ||
|
|
5e20d1d554 | ||
|
|
18595ca86a | ||
|
|
3e269fa188 | ||
|
|
65405c2cb9 | ||
|
|
442cf8897a | ||
|
|
d5fb275e9e | ||
|
|
e60aec81ca | ||
|
|
398e9e535e | ||
|
|
08bf651ef2 | ||
|
|
6171de41bf | ||
|
|
f0a56592e2 | ||
|
|
87a7d4d1a8 | ||
|
|
05287902a2 | ||
|
|
96e453feb6 | ||
|
|
9c0fa9fc04 | ||
|
|
3bde0715b0 | ||
|
|
e2ec3d8b9b | ||
|
|
ad91eaf8a7 | ||
|
|
b6d30214fd | ||
|
|
c4ab01d63d | ||
|
|
4ba3b3f55a | ||
|
|
52d2706a9e | ||
|
|
964afc69f6 | ||
|
|
3ddf545ccd | ||
|
|
f9374733bb | ||
|
|
5930135f45 | ||
|
|
f03f6bc128 | ||
|
|
6c50c70235 | ||
|
|
8790fc2c1b | ||
|
|
f86c4fccf4 | ||
|
|
7d0785e9ed | ||
|
|
2cac88162c | ||
|
|
4809627d8a | ||
|
|
871979abd6 | ||
|
|
efb95722ca | ||
|
|
d9bbb80a6b | ||
|
|
3254315127 | ||
|
|
ca297fd26b | ||
|
|
ac36a43cef | ||
|
|
f00183115d | ||
|
|
161b29a899 | ||
|
|
72253d09fa | ||
|
|
40d09ddb23 | ||
|
|
3026d86a9e | ||
|
|
0661a7edc3 | ||
|
|
24b0adfacc | ||
|
|
acc6698ab3 | ||
|
|
606d3e6aa1 | ||
|
|
687a7954d6 | ||
|
|
36a53a7b37 | ||
|
|
0a5982a902 | ||
|
|
601863f9e9 | ||
|
|
c9ce731119 | ||
|
|
abed8e034e | ||
|
|
63032d304d | ||
|
|
a57ecede78 | ||
|
|
b0ce7c63dd | ||
|
|
d139a11c16 | ||
|
|
aef043db0b | ||
|
|
b8f3ead541 | ||
|
|
e0bb898f29 | ||
|
|
45336c7c28 | ||
|
|
20aabb2e83 | ||
|
|
1539e24d61 | ||
|
|
c7cf041e4a | ||
|
|
da80d3f354 | ||
|
|
001c8d7678 | ||
|
|
38ab03655b | ||
|
|
9226f8a5d1 | ||
|
|
5c8a007f3e | ||
|
|
b3ad3e297d | ||
|
|
d607553e48 | ||
|
|
7cf83c77ca | ||
|
|
8a9f174f63 | ||
|
|
98a0786c32 | ||
|
|
df1129724c | ||
|
|
423cef08bf | ||
|
|
04580accb4 | ||
|
|
6376f77b8c | ||
|
|
e27e614ed9 | ||
|
|
b0c04704a1 | ||
|
|
6bb6bf8323 | ||
|
|
e22fe8aefc | ||
|
|
76276f61e5 | ||
|
|
bfd26e6ec6 | ||
|
|
d787e1ea0f | ||
|
|
b5d7e9cbb0 | ||
|
|
f3b6d9dcdf | ||
|
|
a9abe13185 | ||
|
|
6b315e8315 | ||
|
|
37677de884 | ||
|
|
c7387de325 | ||
|
|
2495b1e038 | ||
|
|
073ee52ce7 | ||
|
|
54150a14e9 | ||
|
|
88ff091cce | ||
|
|
e87a5776f1 | ||
|
|
0806ce6406 | ||
|
|
6409894a71 | ||
|
|
e7286f6129 | ||
|
|
2ab94b3151 | ||
|
|
7ee90890ec | ||
|
|
383e726d65 | ||
|
|
e046f70642 | ||
|
|
2131ad4670 | ||
|
|
219fe2155b | ||
|
|
4209034d20 | ||
|
|
abcae0c2a4 | ||
|
|
0934905493 | ||
|
|
11cd6201d9 | ||
|
|
8d2a917676 | ||
|
|
44b4afa534 | ||
|
|
775be3933c | ||
|
|
df87e21c85 | ||
|
|
d761d80750 | ||
|
|
8052019dde | ||
|
|
a3d8950088 | ||
|
|
004f5d3bf1 | ||
|
|
f5d308a156 | ||
|
|
3869996758 | ||
|
|
cdb2107c4e | ||
|
|
4db2b3413b | ||
|
|
b2f31bec79 | ||
|
|
78f9f4a266 | ||
|
|
ad6087c342 | ||
|
|
0d6ef430de | ||
|
|
a5942209e8 | ||
|
|
9a60694cfc | ||
|
|
4bf13f4737 | ||
|
|
9e89b75186 | ||
|
|
0b10db91be | ||
|
|
1a516b2af9 | ||
|
|
076363d78e | ||
|
|
5fde214290 | ||
|
|
a620724d6a | ||
|
|
640b953ec7 | ||
|
|
a009ca7597 | ||
|
|
7368399f8b | ||
|
|
c974aec934 | ||
|
|
3033f03f64 | ||
|
|
72723e0bb5 | ||
|
|
2fb6ab3939 | ||
|
|
25c1c160b8 | ||
|
|
7e92895471 | ||
|
|
d291d48991 | ||
|
|
0e6b8042b0 | ||
|
|
34c78a892a | ||
|
|
9d28879505 | ||
|
|
2482296e2b | ||
|
|
f755fb76ee | ||
|
|
c61b5dcb62 | ||
|
|
fae893b9d9 | ||
|
|
10aadefd6a | ||
|
|
e75b6280fd | ||
|
|
8c4023165a | ||
|
|
b7d403f106 | ||
|
|
b069de0caa | ||
|
|
136da74bfa | ||
|
|
7fc897e6dc | ||
|
|
9b731d63b8 | ||
|
|
10aa59f674 | ||
|
|
1f7837e7b1 | ||
|
|
ba0535e3fb | ||
|
|
49fa7f6b5c | ||
|
|
c95db246d4 | ||
|
|
1ba93371ce | ||
|
|
fedbbdb575 | ||
|
|
85ebba72bc | ||
|
|
b6cd436d5d | ||
|
|
ec170c7e1e | ||
|
|
f6399eb90f | ||
|
|
77f2448e59 | ||
|
|
3d69b46fca | ||
|
|
4b6153ad18 | ||
|
|
75d37eb103 | ||
|
|
11b6f77df0 | ||
|
|
db8b0319dd | ||
|
|
c9dd330766 | ||
|
|
e40228102c | ||
|
|
7889c6fb4c | ||
|
|
6eb1773110 | ||
|
|
1d25823746 | ||
|
|
d1d4f1e198 | ||
|
|
709c01c7a1 | ||
|
|
4a341c9034 | ||
|
|
be41ff6d54 | ||
|
|
1dbb6f1746 | ||
|
|
753e6274ab | ||
|
|
7f462c618b | ||
|
|
d8ac6e28ab | ||
|
|
a95ffcdc46 | ||
|
|
1b01d45dd2 | ||
|
|
7a1cd39b21 | ||
|
|
1c1fd9616a | ||
|
|
11e19e4085 | ||
|
|
2a43f73228 | ||
|
|
b1d1310a75 | ||
|
|
0e7fa78e65 | ||
|
|
4032570d97 | ||
|
|
90644a3017 | ||
|
|
4f6bffb477 | ||
|
|
158f902c3b | ||
|
|
6dc25ddc6e | ||
|
|
7f6aaeaecf | ||
|
|
ace439910e | ||
|
|
7f038568de | ||
|
|
af777c0b6a | ||
|
|
fc299032a4 | ||
|
|
e0f3f07907 | ||
|
|
b36df9cf9e | ||
|
|
81c3f780d4 | ||
|
|
b51efdd3e3 | ||
|
|
610b769df9 | ||
|
|
527f4d0101 | ||
|
|
8d9be43c60 | ||
|
|
40ef4f0bbe | ||
|
|
d0271d5049 | ||
|
|
5becfcf8ea | ||
|
|
112e8d6c18 | ||
|
|
1d8d49a01d | ||
|
|
5050155685 | ||
|
|
a9bd494cc0 | ||
|
|
6a4df78bc0 | ||
|
|
530eae3898 | ||
|
|
3e444f6a90 | ||
|
|
45dbff6401 | ||
|
|
bc56b8e058 | ||
|
|
d86e315c48 | ||
|
|
746969207a | ||
|
|
1caebaefb5 | ||
|
|
2d10fdcf0f | ||
|
|
355ec70a80 | ||
|
|
a2f499de01 | ||
|
|
f4bca89722 | ||
|
|
9fbc69df3f | ||
|
|
230d301268 | ||
|
|
1ce7b02d94 | ||
|
|
a2d00f5f1d | ||
|
|
f68eaa3b46 | ||
|
|
0199ab220e | ||
|
|
656045610a | ||
|
|
8c1c61f207 | ||
|
|
af085b79dd | ||
|
|
77476965ae | ||
|
|
961c1365f9 | ||
|
|
ca51514046 | ||
|
|
8975b72a01 | ||
|
|
874ec6a87f | ||
|
|
909eaeeead | ||
|
|
c138161fae | ||
|
|
e48590d66c | ||
|
|
5b1c8541fc | ||
|
|
e5e011021b | ||
|
|
11d74dea09 | ||
|
|
cbdf9c88c5 | ||
|
|
46601b1350 | ||
|
|
6f1a40b2ca | ||
|
|
a2b1f54eb2 | ||
|
|
6756016572 | ||
|
|
f42123afc3 | ||
|
|
1425ffd274 | ||
|
|
d700154e0e | ||
|
|
efecf42566 | ||
|
|
74bdfc07fb | ||
|
|
376dfdba1c | ||
|
|
3795d6720f | ||
|
|
537aaf56d7 | ||
|
|
34d51b5d3d | ||
|
|
dea8fcfb5b | ||
|
|
4f1f3b9b51 | ||
|
|
dfeb8812ad | ||
|
|
63e2b4273a | ||
|
|
5790dbc085 | ||
|
|
9e2105e08d | ||
|
|
22582bbd1c | ||
|
|
e5f27b7a12 | ||
|
|
e88ec9822b | ||
|
|
5ffd2f5c96 | ||
|
|
11fdb4c5d8 | ||
|
|
319aff6d09 | ||
|
|
a614fa3400 | ||
|
|
8d691391ac | ||
|
|
0089a84c94 | ||
|
|
90676e1c6a | ||
|
|
062901be43 | ||
|
|
b195d79b50 | ||
|
|
6d7ee98721 | ||
|
|
fc0800ed5d | ||
|
|
f4e3a0e5b2 | ||
|
|
d631c80024 | ||
|
|
f1f0033875 | ||
|
|
84d120e850 | ||
|
|
8159cc6b88 | ||
|
|
995f8c106b | ||
|
|
7cc104b138 | ||
|
|
b3a7299a62 | ||
|
|
0e7a4deaec | ||
|
|
b4d66650bd | ||
|
|
4986afca28 | ||
|
|
2b6004a82b | ||
|
|
04c54a7c31 | ||
|
|
7ae6c5ae87 | ||
|
|
4046766ca5 | ||
|
|
810390df0b | ||
|
|
de749bc7ae | ||
|
|
9965b8800c | ||
|
|
ab870fddd6 | ||
|
|
a79d6807cf | ||
|
|
8185fb7e43 | ||
|
|
4b10929b25 | ||
|
|
f6c70312c9 | ||
|
|
9fd9c7a51f | ||
|
|
422e619978 | ||
|
|
e6e34251c6 | ||
|
|
2d8aad1086 | ||
|
|
74ca736333 | ||
|
|
5e4fd8b0b9 | ||
|
|
8ab8132411 | ||
|
|
6899dd46e4 | ||
|
|
8ad0697a20 | ||
|
|
b47e5672e6 | ||
|
|
45c7bd9a60 | ||
|
|
e7bcb95635 | ||
|
|
3de83627a9 | ||
|
|
f248576994 | ||
|
|
aac5b6de3b | ||
|
|
24435f11e0 | ||
|
|
a9da839c39 | ||
|
|
fa2c0296d6 | ||
|
|
1dfc32d7e6 | ||
|
|
4a61beae41 | ||
|
|
bd30587bf1 | ||
|
|
e0070e3e18 | ||
|
|
019513696b | ||
|
|
ad7a4476db | ||
|
|
4812b20fb2 | ||
|
|
f5e07c9427 | ||
|
|
75dcb90621 | ||
|
|
dfc0434cc2 | ||
|
|
882fc2257c | ||
|
|
41e83b52fc | ||
|
|
47758b4d8f | ||
|
|
6bf1f970a0 | ||
|
|
7d451f101f | ||
|
|
7edbfe0e40 | ||
|
|
91b42cbfa8 | ||
|
|
6907df41b4 | ||
|
|
2cebd90cbd | ||
|
|
376a121aaa | ||
|
|
da11fd17ee | ||
|
|
a40689a0ff | ||
|
|
44a45fc3fb | ||
|
|
ec4bb5359a | ||
|
|
d2217632df | ||
|
|
2cc044feed | ||
|
|
64fd0cb54f | ||
|
|
c5a1d22e81 | ||
|
|
a7b307af04 | ||
|
|
3269eba16c | ||
|
|
d472860e3b | ||
|
|
731c9ea55e | ||
|
|
0b6af8d965 | ||
|
|
92ca9e954c | ||
|
|
67aa00e2c5 | ||
|
|
a1bf161eb7 | ||
|
|
56614fcaa4 | ||
|
|
4d9169e15f | ||
|
|
3a167af2c4 | ||
|
|
64b9005030 | ||
|
|
965de3a235 | ||
|
|
2040ae4856 | ||
|
|
31a1f9f5ea | ||
|
|
7eadd82048 | ||
|
|
31d0eaac8e | ||
|
|
f45d012635 | ||
|
|
5a777ee9bc | ||
|
|
7bbf6bc7f4 | ||
|
|
9bfc45702d | ||
|
|
40aa82ab41 | ||
|
|
5d2c67c62b | ||
|
|
a00ef4836d | ||
|
|
3ef766bb93 | ||
|
|
44b5a18462 | ||
|
|
4df716f0e8 | ||
|
|
fcbf34a4d3 | ||
|
|
c7b8b6e18b | ||
|
|
9fd165bf4b | ||
|
|
2061092ec5 | ||
|
|
57466a31ba | ||
|
|
4213c69bef | ||
|
|
cc5578488a | ||
|
|
4b7135f0e5 | ||
|
|
984df023b1 | ||
|
|
6f8682f558 | ||
|
|
51defa6d66 | ||
|
|
7d73098d6e | ||
|
|
c580aa4683 | ||
|
|
47d1199360 | ||
|
|
61ab435b31 | ||
|
|
aa8f534b45 | ||
|
|
dc9589aca8 | ||
|
|
dd6eaacc6b | ||
|
|
37dc03eec6 | ||
|
|
6b478172f6 | ||
|
|
c6e73bcfd6 | ||
|
|
a2d62938ce | ||
|
|
d7ae1f3cca | ||
|
|
70219581c4 | ||
|
|
f70ac9fb89 | ||
|
|
235b9fbaf0 | ||
|
|
ebda7f42db | ||
|
|
0b04e4b977 | ||
|
|
9498601a37 | ||
|
|
ef5d320e06 | ||
|
|
b00c9a562d | ||
|
|
5372656893 | ||
|
|
571de0e368 | ||
|
|
82cea2fd85 | ||
|
|
aed9814345 | ||
|
|
34fc1f5fd7 | ||
|
|
87c2ed8b27 | ||
|
|
1467d118ab | ||
|
|
922dbe83c3 | ||
|
|
6af7d61ee5 | ||
|
|
bafd08391d | ||
|
|
82ebd8ef1a | ||
|
|
4ed1aa4d23 | ||
|
|
d04e43d46d | ||
|
|
952f0cca15 | ||
|
|
f6a4d8f1f8 | ||
|
|
b3097a2384 | ||
|
|
6d9ddbe98b | ||
|
|
9bb42c0229 | ||
|
|
bd7226b27a | ||
|
|
5b413e3873 | ||
|
|
be5831a629 | ||
|
|
084d2bf8e2 | ||
|
|
da79e6bac7 | ||
|
|
c4831ac00c | ||
|
|
93a954ef9f | ||
|
|
f7ce8f44e9 | ||
|
|
0b012697e5 | ||
|
|
58e357c992 | ||
|
|
71fbad83ad | ||
|
|
52483072dc | ||
|
|
7f0b8621f3 | ||
|
|
cd8db60b06 | ||
|
|
1aa34f5d2e | ||
|
|
dfa1d88ce9 | ||
|
|
dd38519f07 | ||
|
|
098f5d4f0b | ||
|
|
ffc685d536 | ||
|
|
cd1a99a0de | ||
|
|
48e3b267fc | ||
|
|
3a7c3417bb | ||
|
|
d792ef7222 | ||
|
|
2c24f67deb | ||
|
|
9e75e28d0c | ||
|
|
3232643809 | ||
|
|
f7ee9e90ce | ||
|
|
47298be132 | ||
|
|
a88fa83515 | ||
|
|
12bfe20385 | ||
|
|
3d2f6f0772 | ||
|
|
1cb607f64b | ||
|
|
d3c54fbbde | ||
|
|
28341b755f | ||
|
|
4b5cd420e1 | ||
|
|
1d57bcc99e | ||
|
|
facdd13879 | ||
|
|
6e891f91d3 | ||
|
|
9b50ede977 | ||
|
|
82cf010333 | ||
|
|
6ff6c8614f | ||
|
|
eb1cd38f6c | ||
|
|
148b632b4f | ||
|
|
591e213713 | ||
|
|
75f2262659 | ||
|
|
d9005a1074 | ||
|
|
3e73fa81bf | ||
|
|
ba6e290231 | ||
|
|
08e47117a3 | ||
|
|
532ef38157 | ||
|
|
4c09875890 | ||
|
|
0e98139712 | ||
|
|
4c04d802d7 | ||
|
|
b3dc404571 | ||
|
|
8694f8d2eb | ||
|
|
263f9b79f4 | ||
|
|
56d2aae963 | ||
|
|
127706153d | ||
|
|
caee5b1428 | ||
|
|
6c12e7e944 | ||
|
|
cd04ae6949 | ||
|
|
6a0b68298f | ||
|
|
0a2f732267 | ||
|
|
4bade99f27 | ||
|
|
0b048cd24e | ||
|
|
c69ee63d82 | ||
|
|
744fa104d7 | ||
|
|
e24ff0fd64 | ||
|
|
5de107d44c | ||
|
|
131a5b741d | ||
|
|
65b89687a9 | ||
|
|
048ae40e75 | ||
|
|
234183ecd2 | ||
|
|
fb067dc97b | ||
|
|
a1fea0ce16 | ||
|
|
e1e9135e93 | ||
|
|
aff982036b | ||
|
|
d087649eab | ||
|
|
7f3fa46a40 | ||
|
|
b1f79e4d97 | ||
|
|
115d6df94f | ||
|
|
559af9635f | ||
|
|
cb06359c0b | ||
|
|
5e26bb29d9 | ||
|
|
b0e95842b8 | ||
|
|
08e678f21f | ||
|
|
c17817810f | ||
|
|
ff5c38b1f7 | ||
|
|
64314c1b82 | ||
|
|
83230097ae | ||
|
|
8f91acf956 | ||
|
|
d211722a2f | ||
|
|
56e6ed1249 | ||
|
|
21982cf1cb | ||
|
|
edc01408da | ||
|
|
aee33c87ed | ||
|
|
0dae1602c7 | ||
|
|
d926f07ac1 | ||
|
|
96045e98f4 | ||
|
|
01b7205e2c | ||
|
|
c8a4cbcf17 | ||
|
|
16b6442b23 | ||
|
|
183eafa587 | ||
|
|
47a2997538 | ||
|
|
37ebcadfa1 | ||
|
|
74d98216f1 | ||
|
|
4bdebf573e | ||
|
|
1606b6a383 | ||
|
|
2a61902df5 | ||
|
|
01a1c2b576 | ||
|
|
c4f01de231 | ||
|
|
63a4a761dd | ||
|
|
d077c03686 | ||
|
|
c97ea1f2a9 | ||
|
|
fd27df2abb | ||
|
|
bf04f03c4c | ||
|
|
93e802f473 | ||
|
|
1464b9087a | ||
|
|
e8cc8fc879 | ||
|
|
fae2119b1e | ||
|
|
aa859a4139 | ||
|
|
b9b12e2879 | ||
|
|
cf643c9f43 | ||
|
|
5b1a7880a9 | ||
|
|
474b6b0500 | ||
|
|
6c8c1d8173 | ||
|
|
6a91fa637f | ||
|
|
2846fb4e31 | ||
|
|
a1033cdc64 | ||
|
|
204336e1a5 | ||
|
|
8954e6c3b9 | ||
|
|
fee22b6b0b | ||
|
|
2b82c31b85 | ||
|
|
9a4813089c | ||
|
|
554fcc8b9d | ||
|
|
345256ee99 | ||
|
|
58d1042147 | ||
|
|
7b7e3a3e03 | ||
|
|
1e7fbd4202 | ||
|
|
6e907856f2 | ||
|
|
8bc6019172 | ||
|
|
059f79242e | ||
|
|
89599b4812 | ||
|
|
a9f4047a97 | ||
|
|
23227ae763 | ||
|
|
4a9e9e9db2 | ||
|
|
88ef2718f1 | ||
|
|
e71e8ca3ad | ||
|
|
45e9257d6e | ||
|
|
2954e72652 | ||
|
|
199de96cff | ||
|
|
8ddbe81513 | ||
|
|
a3e26e0498 | ||
|
|
4ad129d8d8 | ||
|
|
dfb9fa0736 | ||
|
|
eb036898e9 | ||
|
|
7c6aa76a2a | ||
|
|
f035cb1088 | ||
|
|
35162166c5 | ||
|
|
107f6abcb1 | ||
|
|
760a939e7d | ||
|
|
72660d0dec | ||
|
|
8444a8f211 | ||
|
|
4a1fec8328 | ||
|
|
42547f6017 | ||
|
|
0ccf564f03 | ||
|
|
65c9a07dde | ||
|
|
4700a19322 | ||
|
|
3d3b3abc1b | ||
|
|
7cd2770a13 | ||
|
|
7b94129d9e | ||
|
|
d1a0065ef8 | ||
|
|
5a817370fd | ||
|
|
ab0a210763 | ||
|
|
9f800736bc | ||
|
|
c9a83afad6 | ||
|
|
5e14274f10 | ||
|
|
167470b4bd | ||
|
|
f06d3c2ec2 | ||
|
|
74c99a8a77 | ||
|
|
0e4d312ee2 | ||
|
|
589f19559d | ||
|
|
f28bc25dc0 | ||
|
|
a0657ad937 | ||
|
|
5b8d88af4c | ||
|
|
fa82b50340 | ||
|
|
005216bc57 | ||
|
|
e748fdcf6f | ||
|
|
8c17c9918e | ||
|
|
ea0dd99d0b | ||
|
|
e0cc67afae | ||
|
|
04f9cbe364 | ||
|
|
99afebd033 | ||
|
|
a6feacc810 | ||
|
|
65e4b1672f | ||
|
|
46cc0dd190 | ||
|
|
678b9fb603 | ||
|
|
49ab0c1f0b | ||
|
|
ab490a7736 | ||
|
|
e4ce1dae35 | ||
|
|
179b812acb | ||
|
|
7f170517ec | ||
|
|
5480da4f04 | ||
|
|
9a15a4db10 | ||
|
|
55aeaec293 | ||
|
|
f6df1fb40c | ||
|
|
b889a89c36 | ||
|
|
1976dc6f30 | ||
|
|
e864c65d26 | ||
|
|
02fba02d31 | ||
|
|
fb9e7c82f6 | ||
|
|
77d31bf646 | ||
|
|
29ca799bcf | ||
|
|
467b7f0163 | ||
|
|
bad67c6dc5 | ||
|
|
ac40426971 | ||
|
|
7acfaf6d34 | ||
|
|
99e47c9c04 | ||
|
|
d7904e2251 | ||
|
|
68aef489de | ||
|
|
3f9adcd5e0 | ||
|
|
6cc5135d2d | ||
|
|
d4c72b371f | ||
|
|
18b6f05657 | ||
|
|
c42d9baa26 | ||
|
|
6e27ecd2b9 | ||
|
|
482692396e | ||
|
|
c48acf165a | ||
|
|
9e004c3ec0 | ||
|
|
7be4e9c919 | ||
|
|
5ec38a4bed | ||
|
|
f246779b8e | ||
|
|
a7d8cdf061 | ||
|
|
620745c812 | ||
|
|
b8767e5ba9 | ||
|
|
d33a50660d | ||
|
|
4ee9658e97 | ||
|
|
dd1b84e7ba | ||
|
|
4c677e6c47 | ||
|
|
f0f889440b | ||
|
|
cc9ceaeb74 | ||
|
|
ad2fa8d1d7 | ||
|
|
adc1580742 | ||
|
|
4d3b44d6df | ||
|
|
e57aa0eee2 | ||
|
|
1ae1d116c7 | ||
|
|
097a69d07f | ||
|
|
a81ce87a50 | ||
|
|
88be0d43a0 | ||
|
|
ff16a00a3d | ||
|
|
8982b3e1e2 | ||
|
|
be0fa35d14 | ||
|
|
9f51ed9d01 | ||
|
|
731e6792c7 | ||
|
|
c35ec0b4aa | ||
|
|
03aaf575dc | ||
|
|
9a060579ba | ||
|
|
d40a5c4f7a | ||
|
|
21f7dc3377 | ||
|
|
43c13a1ed9 | ||
|
|
6bc3f189e1 | ||
|
|
2c5437135c | ||
|
|
444da02523 | ||
|
|
00e8af2381 | ||
|
|
401b21864f | ||
|
|
de939951d4 | ||
|
|
7725d16a26 | ||
|
|
8a74408d83 | ||
|
|
3d0dc95a06 | ||
|
|
04a57a3cc2 | ||
|
|
d0c22ce01d | ||
|
|
23c95e9660 | ||
|
|
eecab9b95d | ||
|
|
8abc2f113c | ||
|
|
949d2ff1c2 | ||
|
|
1c8b763d53 | ||
|
|
bb91393b85 | ||
|
|
cc9c0d819e | ||
|
|
a72b8caf47 | ||
|
|
fdd9b8b8ce | ||
|
|
c096b4ca8c | ||
|
|
427add3008 | ||
|
|
c45871700d | ||
|
|
6821e8eeb2 | ||
|
|
a4f07756a5 |
@@ -0,0 +1,80 @@
|
||||
# OCRmyPDF
|
||||
#
|
||||
|
||||
FROM ubuntu:21.04 as base
|
||||
|
||||
ENV LANG=C.UTF-8
|
||||
ENV TZ=UTC
|
||||
RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
python3 \
|
||||
libqpdf-dev \
|
||||
zlib1g \
|
||||
liblept5
|
||||
|
||||
FROM base as builder
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
build-essential autoconf automake libtool \
|
||||
libleptonica-dev \
|
||||
zlib1g-dev \
|
||||
python3-dev \
|
||||
python3-distutils \
|
||||
libffi-dev \
|
||||
ca-certificates \
|
||||
curl \
|
||||
git
|
||||
|
||||
# Get the latest pip (Ubuntu version doesn't support manylinux2010)
|
||||
RUN \
|
||||
curl https://bootstrap.pypa.io/get-pip.py | python3
|
||||
|
||||
# Compile and install jbig2
|
||||
# Needs libleptonica-dev, zlib1g-dev
|
||||
RUN \
|
||||
mkdir jbig2 \
|
||||
&& curl -L https://github.com/agl/jbig2enc/archive/ea6a40a.tar.gz | \
|
||||
tar xz -C jbig2 --strip-components=1 \
|
||||
&& cd jbig2 \
|
||||
&& ./autogen.sh && ./configure && make && make install \
|
||||
&& cd .. \
|
||||
&& rm -rf jbig2
|
||||
|
||||
COPY . /app
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN pip3 install --no-cache-dir .[test,webservice,watcher]
|
||||
|
||||
FROM base
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ghostscript \
|
||||
img2pdf \
|
||||
libsm6 libxext6 libxrender-dev \
|
||||
pngquant \
|
||||
tesseract-ocr \
|
||||
tesseract-ocr-chi-sim \
|
||||
tesseract-ocr-deu \
|
||||
tesseract-ocr-eng \
|
||||
tesseract-ocr-fra \
|
||||
tesseract-ocr-por \
|
||||
tesseract-ocr-spa \
|
||||
unpaper \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /usr/local/lib/ /usr/local/lib/
|
||||
COPY --from=builder /usr/local/bin/ /usr/local/bin/
|
||||
|
||||
COPY --from=builder /app/misc/webservice.py /app/
|
||||
COPY --from=builder /app/misc/watcher.py /app/
|
||||
|
||||
# Copy minimal project files to get the test suite.
|
||||
COPY --from=builder /app/setup.cfg /app/setup.py /app/README.md /app/
|
||||
COPY --from=builder /app/requirements /app/requirements
|
||||
COPY --from=builder /app/tests /app/tests
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/ocrmypdf"]
|
||||
@@ -1,31 +1,44 @@
|
||||
# dotfiles
|
||||
.*
|
||||
!.coveragerc
|
||||
!.dockerignore
|
||||
!.git_archival.txt
|
||||
!.gitattributes
|
||||
!.gitignore
|
||||
!.pre-commit-config.yaml
|
||||
!.readthedocs.yml
|
||||
|
||||
# Dev scratch
|
||||
*.ipynb
|
||||
*.pdf
|
||||
*.pyc
|
||||
*.rst
|
||||
*.sublime*
|
||||
*/*.pyc
|
||||
*/*/*.pyc
|
||||
*/*/*/*.pyc
|
||||
*/*/*/*/*.pyc
|
||||
*/*/*/*/*/*.pyc
|
||||
*/*/*/*/*/*/*.pyc
|
||||
*/*/*/*/*/*/*/*.pyc
|
||||
.cache/
|
||||
.git/
|
||||
.ipynb_checkpoints/
|
||||
.ruffus_history.sqlite
|
||||
bin/
|
||||
build/
|
||||
dist/
|
||||
htmlcov/
|
||||
include/
|
||||
lib/
|
||||
MANIFEST.in
|
||||
ocrmypdf.egg-info/
|
||||
staging/
|
||||
tests/cache/
|
||||
tests/output/
|
||||
**/*.pyc
|
||||
/*.pdf
|
||||
/*.qdf
|
||||
/*.png
|
||||
/scratch.py
|
||||
IDEAS
|
||||
log/
|
||||
tests/resources/private/
|
||||
tmp/
|
||||
venv-3.4/
|
||||
venv-3.5/
|
||||
venv*/
|
||||
/debug_tests.py
|
||||
*.traineddata
|
||||
/private
|
||||
|
||||
# Package building
|
||||
*.egg-info/
|
||||
build/
|
||||
dist/
|
||||
wheelhouse/
|
||||
pip-wheel-metadata/
|
||||
|
||||
# Code coverage
|
||||
htmlcov/
|
||||
|
||||
# Docker specific
|
||||
bin/
|
||||
docs/
|
||||
include/
|
||||
lib/
|
||||
|
||||
# Docker include .git/
|
||||
!.git/
|
||||
|
||||
@@ -6,5 +6,9 @@
|
||||
*.jar binary
|
||||
*.pdf binary
|
||||
*.PDF binary
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.bin binary
|
||||
*.afdesign binary
|
||||
|
||||
.git_archival.txt export-subst
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: james-barlow
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
@@ -1,3 +0,0 @@
|
||||
Please include the command line and (if needed) a test file with your issue report.
|
||||
|
||||
If possible, please use a test file that we can include in future test cases (no personal information, no copyrighted material).
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
name: General issues
|
||||
about: Installation, packages, dependencies, "nothing works", test suite failures...
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
What's the problem?
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior.
|
||||
|
||||
**Expected behavior**
|
||||
What did you expected to happen?
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**System (please complete the following information):**
|
||||
- OS:
|
||||
- Python version:
|
||||
- OCRmyPDF version:
|
||||
|
||||
**Installation**
|
||||
How did you install OCRmyPDF? Did you install it from your operating system's
|
||||
package manager, or using pip?
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
name: Problem with a specific input file
|
||||
about: Something went wrong while trying to OCR a specific file
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
What command line or API call were you trying to run?
|
||||
|
||||
```bash
|
||||
ocrmypdf ...arguments... input.pdf output.pdf
|
||||
```
|
||||
|
||||
Run with verbosity or higher `-v1` to see more detailed logging. This information may be helpful.
|
||||
|
||||
**Example file**
|
||||
If your issue is a problem that affects only certain files, and we will require an input file (PDF or image) that demonstrates your issue.
|
||||
|
||||
Please provide an input file with no personal or confidential information. At your option you may [GPG-encrypt the file](https://github.com/jbarlow83/OCRmyPDF/wiki) for OCRmyPDF's author only.
|
||||
|
||||
Links to files hosted elsewhere are perfectly acceptable. You could also look in ``tests/resources`` and see if any of those files reproduce your issue.
|
||||
|
||||
*(Issues without example files usually cannot be resolved. It's like reporting an issue against a web browser without providing a URL.)*
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**System**
|
||||
- OS: [e.g. Linux, Windows, macOS]
|
||||
- OCRmyPDF Version: ``ocrmypdf --version``
|
||||
- How did you install ocrmypdf? Did you use a system package manager, `pip`, or a Docker image?
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered. Please include the versions of OCRmyPDF and other supporting programs (Tesseract OCR, Ghostscript) - maybe an alternative already exists in a newer version.
|
||||
|
||||
**Example file**
|
||||
If your issue concerns how OCRmyPDF processes certain files, and please provide an example file that helps illustrate how OCRmyPDF's output could be improve.
|
||||
|
||||
Please provide an input file with no personal or confidential information. At your option you may [GPG-encrypt the file](https://github.com/jbarlow83/OCRmyPDF/wiki) for OCRmyPDF's author only.
|
||||
|
||||
Links to files hosted elsewhere are perfectly acceptable. You could also look in ``tests/resources`` and see if any of those files reproduce your issue.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -0,0 +1,306 @@
|
||||
name: Test and deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- ci
|
||||
- release/*
|
||||
- feature/*
|
||||
tags:
|
||||
- v*
|
||||
paths-ignore:
|
||||
- README*
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test_linux:
|
||||
name: Test ${{ matrix.os }} with Python ${{ matrix.python }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-18.04
|
||||
python: 3.6
|
||||
- os: ubuntu-18.04
|
||||
python: 3.7
|
||||
- os: ubuntu-20.04
|
||||
python: 3.8
|
||||
- os: ubuntu-20.04
|
||||
python: 3.9
|
||||
- os: ubuntu-latest
|
||||
python: 3.9
|
||||
- os: ubuntu-20.04
|
||||
python: "pypy-3.6"
|
||||
- os: ubuntu-latest
|
||||
python: "pypy-3.7"
|
||||
- os: ubuntu-latest
|
||||
python: 3.9
|
||||
tesseract5: true
|
||||
|
||||
env:
|
||||
OS: ${{ matrix.os }}
|
||||
PYTHON: ${{ matrix.python }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
|
||||
|
||||
- uses: actions/setup-python@v2
|
||||
name: Install Python
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
|
||||
- name: Install Tesseract 5
|
||||
if: matrix.tesseract5
|
||||
run: |
|
||||
sudo add-apt-repository ppa:alex-p/tesseract-ocr-devel
|
||||
|
||||
- name: Install common packages
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
ghostscript \
|
||||
img2pdf \
|
||||
libffi-dev \
|
||||
liblept5 \
|
||||
libsm6 libxext6 libxrender-dev \
|
||||
pngquant \
|
||||
poppler-utils \
|
||||
tesseract-ocr \
|
||||
tesseract-ocr-deu \
|
||||
tesseract-ocr-eng \
|
||||
unpaper \
|
||||
zlib1g
|
||||
|
||||
- name: Install Ubuntu 18.04 packages
|
||||
if: matrix.os == 'ubuntu-18.04'
|
||||
run: |
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
libexempi3
|
||||
|
||||
- name: Install Ubuntu 20.04 packages
|
||||
if: matrix.os == 'ubuntu-20.04'
|
||||
run: |
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
libexempi8
|
||||
|
||||
- name: Install Ubuntu packages for PyPy
|
||||
if: startsWith(matrix.python, 'pypy')
|
||||
run: |
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
libxml2-dev \
|
||||
libxslt1-dev \
|
||||
pypy3-dev
|
||||
|
||||
- name: Install Python packages
|
||||
run: |
|
||||
python -m pip install .[test]
|
||||
|
||||
- name: Report versions
|
||||
run: |
|
||||
tesseract --version
|
||||
gs --version
|
||||
pngquant --version
|
||||
unpaper --version
|
||||
img2pdf --version
|
||||
|
||||
- name: Test
|
||||
run: |
|
||||
python -m pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v1
|
||||
with:
|
||||
files: ./coverage.xml
|
||||
env_vars: OS,PYTHON
|
||||
|
||||
test_macos:
|
||||
name: Test macOS
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest]
|
||||
python: ["3.9"]
|
||||
|
||||
env:
|
||||
OS: ${{ matrix.os }}
|
||||
PYTHON: ${{ matrix.python }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
|
||||
|
||||
- uses: actions/setup-python@v2
|
||||
name: Install Python
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
|
||||
- name: Install Homebrew deps
|
||||
run: |
|
||||
brew update
|
||||
brew install \
|
||||
exempi \
|
||||
ghostscript \
|
||||
jbig2enc \
|
||||
leptonica \
|
||||
openjpeg \
|
||||
pngquant \
|
||||
tesseract
|
||||
|
||||
- name: Install Python packages
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install .[test]
|
||||
|
||||
- name: Report versions
|
||||
run: |
|
||||
tesseract --version
|
||||
gs --version
|
||||
pngquant --version
|
||||
img2pdf --version
|
||||
|
||||
- name: Test
|
||||
run: |
|
||||
python -m pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v1
|
||||
with:
|
||||
files: ./coverage.xml
|
||||
env_vars: OS,PYTHON
|
||||
|
||||
test_windows:
|
||||
name: Test Windows
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [windows-latest]
|
||||
python: ["3.9"]
|
||||
|
||||
env:
|
||||
OS: ${{ matrix.os }}
|
||||
PYTHON: ${{ matrix.python }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
|
||||
|
||||
- uses: actions/setup-python@v2
|
||||
name: Install Python
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
|
||||
- name: Install system packages
|
||||
run: |
|
||||
choco install --yes --no-progress --pre tesseract
|
||||
choco install --yes --no-progress ghostscript
|
||||
choco install --yes --no-progress pngquant
|
||||
|
||||
- name: Install Python packages
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install .[test]
|
||||
|
||||
- name: Test
|
||||
run: |
|
||||
python -m pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v1
|
||||
with:
|
||||
files: ./coverage.xml
|
||||
env_vars: OS,PYTHON
|
||||
|
||||
wheel_sdist_linux:
|
||||
name: Build sdist and wheels
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
|
||||
|
||||
- uses: actions/setup-python@v2
|
||||
name: Install Python
|
||||
with:
|
||||
python-version: "3.6"
|
||||
|
||||
- name: Make wheels and sdist
|
||||
run: |
|
||||
python -m pip install --upgrade pip wheel
|
||||
python setup.py sdist
|
||||
python setup.py bdist_wheel
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
path: |
|
||||
./dist/*.whl
|
||||
./dist/*.tar.gz
|
||||
|
||||
upload_pypi:
|
||||
name: Deploy artifacts to PyPI
|
||||
needs: [wheel_sdist_linux, test_linux, test_macos, test_windows]
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v')
|
||||
steps:
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: artifact
|
||||
path: dist
|
||||
|
||||
- uses: pypa/gh-action-pypi-publish@master
|
||||
with:
|
||||
user: __token__
|
||||
password: ${{ secrets.TOKEN_PYPI }}
|
||||
# repository_url: https://test.pypi.org/legacy/
|
||||
|
||||
docker:
|
||||
name: Build Docker images
|
||||
needs: [wheel_sdist_linux, test_linux, test_macos, test_windows]
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'pull_request'
|
||||
steps:
|
||||
- name: Set image tag to release or branch
|
||||
run: echo "DOCKER_IMAGE_TAG=${GITHUB_REF##*/}" >> $GITHUB_ENV
|
||||
|
||||
- name: If master, set to latest
|
||||
run: echo 'DOCKER_IMAGE_TAG=latest' >> $GITHUB_ENV
|
||||
if: env.DOCKER_IMAGE_TAG == 'master'
|
||||
|
||||
- name: Set Docker Hub repository to username
|
||||
run: echo "DOCKER_REPOSITORY=jbarlow83" >> $GITHUB_ENV
|
||||
|
||||
- name: Set image name
|
||||
run: echo "DOCKER_IMAGE_NAME=ocrmypdf" >> $GITHUB_ENV
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: jbarlow83
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Print image tag
|
||||
run: echo "Building image ${DOCKER_REPOSITORY}/${DOCKER_IMAGE_NAME}:${DOCKER_IMAGE_TAG}"
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
docker buildx build \
|
||||
--push \
|
||||
--platform linux/arm64/v8,linux/amd64 \
|
||||
--tag "${DOCKER_REPOSITORY}/${DOCKER_IMAGE_NAME}:${DOCKER_IMAGE_TAG}" \
|
||||
--file .docker/Dockerfile .
|
||||
@@ -1,34 +1,45 @@
|
||||
# Development environment
|
||||
*.pyc
|
||||
*.sublime-*
|
||||
venv-*/
|
||||
pyvenv.cfg
|
||||
tasks.py
|
||||
# dotfiles
|
||||
.*
|
||||
!.coveragerc
|
||||
!.dockerignore
|
||||
!.git_archival.txt
|
||||
!.gitattributes
|
||||
!.gitignore
|
||||
!.pre-commit-config.yaml
|
||||
!.readthedocs.yaml
|
||||
!.github/
|
||||
!.docker/
|
||||
|
||||
# Dev scratch
|
||||
*.ipynb
|
||||
**/*.pyc
|
||||
/*.pdf
|
||||
/*.qdf
|
||||
/*.png
|
||||
/scratch.py
|
||||
IDEAS
|
||||
log/
|
||||
tests/resources/private/
|
||||
tmp/
|
||||
venv*/
|
||||
/debug_tests.py
|
||||
*.traineddata
|
||||
/private
|
||||
/coverage.xml
|
||||
|
||||
# Package building
|
||||
*.egg-info/
|
||||
.cache/
|
||||
.eggs/
|
||||
build/
|
||||
dist/
|
||||
wheelhouse/
|
||||
pip-wheel-metadata/
|
||||
|
||||
# Code coverage
|
||||
htmlcov/
|
||||
|
||||
# Automatically generated files
|
||||
ocrmypdf/lib/_*.py
|
||||
ocrmypdf/version.py
|
||||
docs/_build/
|
||||
docs/_static/
|
||||
docs/_templates/
|
||||
|
||||
# Code coverage
|
||||
.coverage
|
||||
htmlcov/
|
||||
|
||||
# Testing
|
||||
log/
|
||||
/*.pdf
|
||||
.ipynb_checkpoints/
|
||||
tests/cache/
|
||||
tests/output/
|
||||
tests/resources/private/
|
||||
tmp/
|
||||
docs/Makefile
|
||||
ocrmypdf/lib/_*.py
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.0.1
|
||||
hooks:
|
||||
- id: check-case-conflict
|
||||
- id: check-merge-conflict
|
||||
- id: check-toml
|
||||
- id: check-yaml
|
||||
- id: debug-statements
|
||||
- repo: https://github.com/pycqa/isort
|
||||
rev: 5.9.3
|
||||
hooks:
|
||||
- id: isort
|
||||
args: ["--profile", "black"]
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 21.9b0
|
||||
hooks:
|
||||
- id: black
|
||||
language_version: python
|
||||
exclude: ^src/ocrmypdf/lib/_leptonica.py
|
||||
- repo: https://github.com/asottile/setup-cfg-fmt
|
||||
rev: v1.17.0
|
||||
hooks:
|
||||
- id: setup-cfg-fmt
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v2.26.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: ["--py36-plus"]
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v0.910
|
||||
hooks:
|
||||
- id: mypy
|
||||
additional_dependencies:
|
||||
- types-toml
|
||||
- types-setuptools
|
||||
- types-requests
|
||||
- types-Pillow
|
||||
@@ -0,0 +1,22 @@
|
||||
# Read the Docs configuration file
|
||||
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
||||
|
||||
# Required
|
||||
version: 2
|
||||
|
||||
# Build documentation in the docs/ directory with Sphinx
|
||||
sphinx:
|
||||
configuration: docs/conf.py
|
||||
|
||||
# Optionally build your docs in additional formats such as PDF
|
||||
formats:
|
||||
- pdf
|
||||
|
||||
# Optionally set the version of Python and requirements required to build your docs
|
||||
python:
|
||||
version: 3.7
|
||||
install:
|
||||
- method: pip
|
||||
path: .
|
||||
extra_requirements:
|
||||
- docs
|
||||
@@ -1,45 +0,0 @@
|
||||
sudo: required
|
||||
dist: trusty
|
||||
language: python
|
||||
|
||||
cache: pip
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.ccache
|
||||
- packages
|
||||
- tests/cache
|
||||
|
||||
python:
|
||||
- 3.4
|
||||
- 3.5
|
||||
|
||||
before_cache:
|
||||
- rm -f $HOME/.cache/pip/log/debug.log
|
||||
|
||||
before_install:
|
||||
# Ubuntu packages
|
||||
- sudo add-apt-repository ppa:vshn/ghostscript -y # for ghostscript 9.16 (trusty has 9.10)
|
||||
- sudo add-apt-repository ppa:heyarje/libav-11 -y # for libav11, which is for unpaper
|
||||
- sudo apt-get update -qq # must go after all add-apt-repo
|
||||
- sudo apt-get install -y ghostscript tesseract-ocr tesseract-ocr-deu tesseract-ocr-eng tesseract-ocr-fra qpdf poppler-utils libavformat56 libavcodec56 libavutil54 libffi-dev
|
||||
|
||||
# pip
|
||||
- pip install --upgrade pip
|
||||
|
||||
# Download, make and install unpaper (using ccache)
|
||||
- mkdir -p packages
|
||||
- "[ -f packages/unpaper_6.1-1.deb ] || wget -q https://dl.dropboxusercontent.com/u/28971240/unpaper_6.1-1.deb -O packages/unpaper_6.1-1.deb"
|
||||
- sudo dpkg -i packages/unpaper_6.1-1.deb
|
||||
|
||||
install:
|
||||
- pip install -r requirements.txt
|
||||
- pip install -r test_requirements.txt
|
||||
|
||||
script:
|
||||
- python setup.py clean
|
||||
- python setup.py install
|
||||
- python setup.py test
|
||||
|
||||
os:
|
||||
- linux
|
||||
@@ -1,91 +0,0 @@
|
||||
# OCRmyPDF
|
||||
#
|
||||
# VERSION 3.2
|
||||
FROM debian:stretch
|
||||
MAINTAINER James R. Barlow <jim@purplerock.ca>
|
||||
|
||||
# Add unprivileged user
|
||||
RUN useradd docker \
|
||||
&& mkdir /home/docker \
|
||||
&& chown docker:docker /home/docker
|
||||
|
||||
# Update system and install our dependencies
|
||||
# If this command takes too Docker hub's automated build will timeout,
|
||||
# so try it in portions
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
locales \
|
||||
python3 \
|
||||
python3-pip \
|
||||
python3-venv \
|
||||
python3-reportlab \
|
||||
python3-pil \
|
||||
python3-wheel
|
||||
|
||||
RUN apt-get install -y --no-install-recommends \
|
||||
unpaper \
|
||||
qpdf \
|
||||
poppler-utils \
|
||||
tesseract-ocr \
|
||||
tesseract-ocr-deu tesseract-ocr-spa tesseract-ocr-eng tesseract-ocr-fra
|
||||
|
||||
RUN apt-get install -qy --no-install-recommends \
|
||||
libffi-dev \
|
||||
libpython3-dev \
|
||||
gcc
|
||||
|
||||
# Install Ghostscript from Debian sid to work around JPEG 2000 issue in
|
||||
# Debian stretch libgs9 or gs 9.16~dfsg-2.1
|
||||
|
||||
COPY ./share/etc-apt-sources.list /etc/apt/sources.list
|
||||
|
||||
RUN apt-get update && apt-get install -y ghostscript/sid
|
||||
|
||||
|
||||
# Enforce UTF-8
|
||||
# Borrowed from https://index.docker.io/u/crosbymichael/python/
|
||||
RUN dpkg-reconfigure locales && \
|
||||
locale-gen C.UTF-8 && \
|
||||
/usr/sbin/update-locale LANG=C.UTF-8
|
||||
ENV LC_ALL C.UTF-8
|
||||
|
||||
|
||||
# Set up a Python virtualenv and take all of the system packages, so we can
|
||||
# rely on the platform packages rather than importing GCC and compiling them
|
||||
RUN pyvenv /appenv \
|
||||
&& pyvenv --system-site-packages /appenv
|
||||
|
||||
COPY . /application/
|
||||
|
||||
# Replace stock Tesseract 3.04.00 font with improved sharp2.ttf that resolves
|
||||
# issues in many PDF viewers.
|
||||
# Discussion is in https://github.com/tesseract-ocr/tesseract/issues/182
|
||||
COPY ./share/sharp2.ttf /usr/share/tesseract-ocr/tessdata/pdf.ttf
|
||||
RUN chmod 644 /usr/share/tesseract-ocr/tessdata/pdf.ttf
|
||||
|
||||
# Set this here to force a docker version, allowing non-tagged versions to
|
||||
# be built
|
||||
# ENV SETUPTOOLS_SCM_PRETEND_VERSION=v3.3.0
|
||||
|
||||
# Install application and dependencies
|
||||
# In this arrangement Pillow and reportlab will be provided by the system
|
||||
# Even though ocrmypdf is locally present, pull from PyPI because
|
||||
# Dockerhub and setuptools_scm clash
|
||||
RUN . /appenv/bin/activate; \
|
||||
pip install --upgrade pip \
|
||||
&& pip install ocrmypdf \
|
||||
&& pip install --no-cache-dir -r /application/test_requirements.txt
|
||||
|
||||
# Remove the junk
|
||||
RUN apt-get remove -qy gcc
|
||||
RUN apt-get autoremove -y && apt-get clean -y
|
||||
RUN rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /root/*
|
||||
|
||||
USER docker
|
||||
WORKDIR /home/docker
|
||||
|
||||
ENV OCRMYPDF_TEST_OUTPUT=/tmp/test-output
|
||||
ENV OCRMYPDF_SHARP_TTF=1
|
||||
|
||||
# Must use array form of ENTRYPOINT
|
||||
# Non-array form does not append other arguments, because that is "intuitive"
|
||||
ENTRYPOINT ["/application/docker-wrapper.sh"]
|
||||
@@ -1,16 +0,0 @@
|
||||
# OCRmyPDF polyglot
|
||||
#
|
||||
# VERSION 3.2
|
||||
FROM jbarlow83/ocrmypdf:latest
|
||||
MAINTAINER James R. Barlow <jim@purplerock.ca>
|
||||
|
||||
# Update system and install our dependencies
|
||||
USER root
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
tesseract-ocr-all
|
||||
|
||||
USER docker
|
||||
|
||||
# Must use array form of ENTRYPOINT
|
||||
# Non-array form does not append other arguments, because that is "intuitive"
|
||||
ENTRYPOINT ["/application/docker-wrapper.sh"]
|
||||
@@ -0,0 +1,373 @@
|
||||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
1. Definitions
|
||||
--------------
|
||||
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
(b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
2. License Grants and Conditions
|
||||
--------------------------------
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
(a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
(b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
3. Responsibilities
|
||||
-------------------
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
(a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
(b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
---------------------------------------------------
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
--------------
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 6. Disclaimer of Warranty *
|
||||
* ------------------------- *
|
||||
* *
|
||||
* Covered Software is provided under this License on an "as is" *
|
||||
* basis, without warranty of any kind, either expressed, implied, or *
|
||||
* statutory, including, without limitation, warranties that the *
|
||||
* Covered Software is free of defects, merchantable, fit for a *
|
||||
* particular purpose or non-infringing. The entire risk as to the *
|
||||
* quality and performance of the Covered Software is with You. *
|
||||
* Should any Covered Software prove defective in any respect, You *
|
||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||
* essential part of this License. No use of any Covered Software is *
|
||||
* authorized under this License except under this disclaimer. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 7. Limitation of Liability *
|
||||
* -------------------------- *
|
||||
* *
|
||||
* Under no circumstances and under no legal theory, whether tort *
|
||||
* (including negligence), contract, or otherwise, shall any *
|
||||
* Contributor, or anyone who distributes Covered Software as *
|
||||
* permitted above, be liable to You for any direct, indirect, *
|
||||
* special, incidental, or consequential damages of any character *
|
||||
* including, without limitation, damages for lost profits, loss of *
|
||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||
* and all other commercial damages or losses, even if such party *
|
||||
* shall have been informed of the possibility of such damages. This *
|
||||
* limitation of liability shall not apply to liability for death or *
|
||||
* personal injury resulting from such party's negligence to the *
|
||||
* extent applicable law prohibits such limitation. Some *
|
||||
* jurisdictions do not allow the exclusion or limitation of *
|
||||
* incidental or consequential damages, so this exclusion and *
|
||||
* limitation may not apply to You. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
8. Litigation
|
||||
-------------
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
----------------
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
10. Versions of the License
|
||||
---------------------------
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
-------------------------------------------
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
---------------------------------------------------------
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
||||
@@ -1,20 +0,0 @@
|
||||
Copyright (c) 2013-2016, The OCRmyPDF Authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included
|
||||
in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -1,48 +0,0 @@
|
||||
# requirements
|
||||
include requirements.txt
|
||||
include test_requirements.txt
|
||||
include dev_requirements.txt
|
||||
|
||||
# git
|
||||
include .git_archival.txt
|
||||
|
||||
# docker
|
||||
include Dockerfile
|
||||
include Dockerfile.polyglot
|
||||
include .dockerignore
|
||||
|
||||
# tests
|
||||
include pytest.ini
|
||||
recursive-include tests *.jpg
|
||||
recursive-include tests *.pdf
|
||||
recursive-include tests *.py
|
||||
recursive-include tests *.rst
|
||||
recursive-include tests *.txt
|
||||
recursive-exclude tests/output *
|
||||
recursive-exclude tests/output_pageinfo *
|
||||
recursive-exclude tests/resources/private *
|
||||
recursive-exclude tests/cache *
|
||||
|
||||
# documentation
|
||||
include *.rst
|
||||
include pipeline.svg
|
||||
recursive-exclude .github *
|
||||
recursive-include docs *.py
|
||||
recursive-include docs *.rst
|
||||
recursive-include docs *.svg
|
||||
recursive-exclude docs/_build *
|
||||
|
||||
|
||||
# support files
|
||||
recursive-include ocrmypdf/data *
|
||||
recursive-include share *
|
||||
include *.py
|
||||
exclude tasks.py
|
||||
|
||||
# code
|
||||
recursive-include ocrmypdf *.py
|
||||
exclude ocrmypdf/lib/_leptonica.py
|
||||
include OCRmyPDF.sh
|
||||
include docker-wrapper.sh
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/bin/sh
|
||||
##############################################################################
|
||||
# Copyright (c) 2013-14: fritz-hh from Github (https://github.com/fritz-hh)
|
||||
##############################################################################
|
||||
|
||||
echo "This script is deprecated. Use 'ocrmypdf' instead."
|
||||
|
||||
python3 -m ocrmypdf.main "$@"
|
||||
@@ -0,0 +1,143 @@
|
||||
<img src="docs/images/logo.svg" width="240" alt="OCRmyPDF">
|
||||
|
||||
[](https://github.com/jbarlow83/OCRmyPDF/actions/workflows/build.yml) [![PyPI version][pypi]](https://pypi.org/project/ocrmypdf/) ![Homebrew version][homebrew] ![ReadTheDocs][docs] ![Python versions][pyversions]
|
||||
|
||||
[azure]: https://dev.azure.com/jim0585/ocrmypdf/_apis/build/status/jbarlow83.OCRmyPDF?branchName=master
|
||||
[travis]: https://travis-ci.org/jbarlow83/OCRmyPDF.svg?branch=master "Travis build status"
|
||||
[pypi]: https://img.shields.io/pypi/v/ocrmypdf.svg "PyPI version"
|
||||
[homebrew]: https://img.shields.io/homebrew/v/ocrmypdf.svg "Homebrew version"
|
||||
[docs]: https://readthedocs.org/projects/ocrmypdf/badge/?version=latest "RTD"
|
||||
[pyversions]: https://img.shields.io/pypi/pyversions/ocrmypdf "Supported Python versions"
|
||||
|
||||
OCRmyPDF adds an OCR text layer to scanned PDF files, allowing them to be searched or copy-pasted.
|
||||
|
||||
```bash
|
||||
ocrmypdf # it's a scriptable command line program
|
||||
-l eng+fra # it supports multiple languages
|
||||
--rotate-pages # it can fix pages that are misrotated
|
||||
--deskew # it can deskew crooked PDFs!
|
||||
--title "My PDF" # it can change output metadata
|
||||
--jobs 4 # it uses multiple cores by default
|
||||
--output-type pdfa # it produces PDF/A by default
|
||||
input_scanned.pdf # takes PDF input (or images)
|
||||
output_searchable.pdf # produces validated PDF output
|
||||
```
|
||||
|
||||
[See the release notes for details on the latest changes](https://ocrmypdf.readthedocs.io/en/latest/release_notes.html).
|
||||
|
||||
## Main features
|
||||
|
||||
- Generates a searchable [PDF/A](https://en.wikipedia.org/?title=PDF/A) file from a regular PDF
|
||||
- Places OCR text accurately below the image to ease copy / paste
|
||||
- Keeps the exact resolution of the original embedded images
|
||||
- When possible, inserts OCR information as a "lossless" operation without disrupting any other content
|
||||
- Optimizes PDF images, often producing files smaller than the input file
|
||||
- If requested, deskews and/or cleans the image before performing OCR
|
||||
- Validates input and output files
|
||||
- Distributes work across all available CPU cores
|
||||
- Uses [Tesseract OCR](https://github.com/tesseract-ocr/tesseract) engine to recognize more than [100 languages](https://github.com/tesseract-ocr/tessdata)
|
||||
- Scales properly to handle files with thousands of pages
|
||||
- Battle-tested on millions of PDFs
|
||||
|
||||
For details: please consult the [documentation](https://ocrmypdf.readthedocs.io/en/latest/).
|
||||
|
||||
## Motivation
|
||||
|
||||
I searched the web for a free command line tool to OCR PDF files: I found many, but none of them were really satisfying:
|
||||
|
||||
- Either they produced PDF files with misplaced text under the image (making copy/paste impossible)
|
||||
- Or they did not handle accents and multilingual characters
|
||||
- Or they changed the resolution of the embedded images
|
||||
- Or they generated ridiculously large PDF files
|
||||
- Or they crashed when trying to OCR
|
||||
- Or they did not produce valid PDF files
|
||||
- On top of that none of them produced PDF/A files (format dedicated for long time storage)
|
||||
|
||||
...so I decided to develop my own tool.
|
||||
|
||||
## Installation
|
||||
|
||||
Linux, Windows, macOS and FreeBSD are supported. Docker images are also available, for both x64 and ARM.
|
||||
|
||||
| Operating system | Install command |
|
||||
| ----------------------------- | ------------------------------|
|
||||
| Debian, Ubuntu | ``apt install ocrmypdf`` |
|
||||
| Windows Subsystem for Linux | ``apt install ocrmypdf`` |
|
||||
| Fedora | ``dnf install ocrmypdf`` |
|
||||
| macOS | ``brew install ocrmypdf`` |
|
||||
| LinuxBrew | ``brew install ocrmypdf`` |
|
||||
| FreeBSD | ``pkg install py37-ocrmypdf`` |
|
||||
| Conda | ``conda install ocrmypdf`` |
|
||||
|
||||
For everyone else, [see our documentation](https://ocrmypdf.readthedocs.io/en/latest/installation.html) for installation steps.
|
||||
|
||||
## Languages
|
||||
|
||||
OCRmyPDF uses Tesseract for OCR, and relies on its language packs. For Linux users, you can often find packages that provide language packs:
|
||||
|
||||
```bash
|
||||
# Display a list of all Tesseract language packs
|
||||
apt-cache search tesseract-ocr
|
||||
|
||||
# Debian/Ubuntu users
|
||||
apt-get install tesseract-ocr-chi-sim # Example: Install Chinese Simplified language pack
|
||||
|
||||
# Arch Linux users
|
||||
pacman -S tesseract-data-eng tesseract-data-deu # Example: Install the English and German language packs
|
||||
|
||||
# brew macOS users
|
||||
brew install tesseract-lang
|
||||
```
|
||||
|
||||
You can then pass the `-l LANG` argument to OCRmyPDF to give a hint as to what languages it should search for. Multiple languages can be requested.
|
||||
|
||||
OCRmyPDF supports Tesseract 4.0 and the beta versions of Tesseract 5.0. It will
|
||||
automatically use whichever version it finds first on the `PATH` environment
|
||||
variable. On Windows, if `PATH` does not provide a Tesseract binary, we use
|
||||
the highest version number that is installed according to the Windows Registry.
|
||||
|
||||
## Documentation and support
|
||||
|
||||
Once OCRmyPDF is installed, the built-in help which explains the command syntax and options can be accessed via:
|
||||
|
||||
```bash
|
||||
ocrmypdf --help
|
||||
```
|
||||
|
||||
Our [documentation is served on Read the Docs](https://ocrmypdf.readthedocs.io/en/latest/index.html).
|
||||
|
||||
Please report issues on our [GitHub issues](https://github.com/jbarlow83/OCRmyPDF/issues) page, and follow the issue template for quick response.
|
||||
|
||||
## Requirements
|
||||
|
||||
In addition to the required Python version (3.6+), OCRmyPDF requires external program installations of Ghostscript, Tesseract OCR, QPDF, and Leptonica. OCRmyPDF is pure Python, but uses CFFI to portably generate library bindings. OCRmyPDF works on pretty much everything: Linux, macOS, Windows and FreeBSD.
|
||||
|
||||
## Press & Media
|
||||
|
||||
- [Going paperless with OCRmyPDF](https://medium.com/@ikirichenko/going-paperless-with-ocrmypdf-e2f36143f46a)
|
||||
- [Converting a scanned document into a compressed searchable PDF with redactions](https://medium.com/@treyharris/converting-a-scanned-document-into-a-compressed-searchable-pdf-with-redactions-63f61c34fe4c)
|
||||
- [c't 1-2014, page 59](https://heise.de/-2279695): Detailed presentation of OCRmyPDF v1.0 in the leading German IT magazine c't
|
||||
- [heise Open Source, 09/2014: Texterkennung mit OCRmyPDF](https://heise.de/-2356670)
|
||||
- [heise Durchsuchbare PDF-Dokumente mit OCRmyPDF erstellen](https://www.heise.de/ratgeber/Durchsuchbare-PDF-Dokumente-mit-OCRmyPDF-erstellen-4607592.html)
|
||||
- [Excellent Utilities: OCRmyPDF](https://www.linuxlinks.com/excellent-utilities-ocrmypdf-add-ocr-text-layer-scanned-pdfs/)
|
||||
- [LinuxUser Texterkennung mit OCRmyPDF und Scanbd automatisieren](https://www.linux-community.de/ausgaben/linuxuser/2021/06/texterkennung-mit-ocrmypdf-und-scanbd-automatisieren/)
|
||||
|
||||
## Business enquiries
|
||||
|
||||
OCRmyPDF would not be the software that it is today without companies and users choosing to provide support for feature development and consulting enquiries. We are happy to discuss all enquiries, whether for extending the existing feature set, or integrating OCRmyPDF into a larger system.
|
||||
|
||||
## License
|
||||
|
||||
The OCRmyPDF software is licensed under the Mozilla Public License 2.0
|
||||
(MPL-2.0). This license permits integration of OCRmyPDF with other code,
|
||||
included commercial and closed source, but asks you to publish source-level
|
||||
modifications you make to OCRmyPDF.
|
||||
|
||||
Some components of OCRmyPDF have other licenses, as noted in those files and the
|
||||
``debian/copyright`` file. Most files in ``misc/`` use the MIT license, and the
|
||||
documentation and test files are generally licensed under Creative Commons
|
||||
ShareAlike 4.0 (CC-BY-SA 4.0).
|
||||
|
||||
## Disclaimer
|
||||
|
||||
The software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@@ -1,119 +0,0 @@
|
||||
OCRmyPDF
|
||||
========
|
||||
|
||||
OCRmyPDF adds an OCR text layer to scanned PDF files, allowing them to
|
||||
be searched or copy-pasted.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf # it's a scriptable command line program
|
||||
-l eng+fra # it supports multiple languages
|
||||
--rotate-pages # it can fix pages that are misrotated
|
||||
--deskew # it can deskew crooked PDFs!
|
||||
--title "My PDF" # it can change output metadata
|
||||
--jobs 4 # it uses multiple cores by default
|
||||
--output-type pdfa # it produces PDF/A by default
|
||||
input_scanned.pdf # takes PDF input (or images)
|
||||
output_searchable.pdf # produces validated PDF output
|
||||
|
||||
|
||||
Main features
|
||||
-------------
|
||||
|
||||
- Generates a searchable
|
||||
`PDF/A <https://en.wikipedia.org/?title=PDF/A>`_ file from a regular PDF
|
||||
- Places OCR text accurately below the image to ease copy / paste
|
||||
- Keeps the exact resolution of the original embedded images
|
||||
- When possible, inserts OCR information as a "lossless" operation without rendering vector information
|
||||
- Keeps file size about the same
|
||||
- If requested deskews and/or cleans the image before performing OCR
|
||||
- Validates input and output files
|
||||
- Provides debug mode to enable easy verification of the OCR results
|
||||
- Processes pages in parallel when more than one CPU core is
|
||||
available
|
||||
- Uses `Tesseract OCR <https://github.com/tesseract-ocr/tesseract>`_ engine
|
||||
- Supports more than `100 languages <https://github.com/tesseract-ocr/tessdata>`_ recognized by Tesseract
|
||||
- Battle-tested on thousands of PDFs, a test suite and continuous integration
|
||||
|
||||
For details: please consult the `release notes <RELEASE_NOTES.rst>`_.
|
||||
|
||||
Motivation
|
||||
----------
|
||||
|
||||
I searched the web for a free command line tool to OCR PDF files on
|
||||
Linux/UNIX: I found many, but none of them were really satisfying.
|
||||
|
||||
- Either they produced PDF files with misplaced text under the image (making copy/paste impossible)
|
||||
- Or they did not handle accents and multilingual characters
|
||||
- Or they changed the resolution of the embedded images
|
||||
- Or they generated ridiculously large PDF files
|
||||
- Or they crashed when trying to OCR some of my PDF files
|
||||
- Or they did not produce valid PDF files (even though they were readable with my current PDF reader)
|
||||
- On top of that none of them produced PDF/A files (format dedicated for long time storage)
|
||||
|
||||
...so I decided to develop my own tool (using various existing scripts
|
||||
as an inspiration).
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
Linux, UNIX, and macOS are supported. Windows is not directly supported but there is a Docker image available that runs on Windows.
|
||||
|
||||
Users of Debian 9 or later or Ubuntu 16.10 or later may simply
|
||||
``apt-get install ocrmypdf``.
|
||||
|
||||
For everyone else, `see our documentation <https://ocrmypdf.readthedocs.io/en/latest/installation.html>`_ for installation steps.
|
||||
|
||||
Languages
|
||||
---------
|
||||
|
||||
OCRmyPDF uses Tesseract for OCR, and relies on its language packs. For Linux users,
|
||||
you can often find packages that provide language packs:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# Display a list of all Tesseract language packs
|
||||
apt-cache search tesseract-ocr
|
||||
|
||||
# Debian/Ubuntu users
|
||||
apt-get install tesseract-ocr-chi-sim # Example: Install Chinese Simplified language back
|
||||
|
||||
You can then pass the ``-l LANG`` argument to OCRmyPDF to give a hint as to what languages it should search for. Multiple
|
||||
languages can be requested.
|
||||
|
||||
Documentation and support
|
||||
-------------------------
|
||||
|
||||
Once ocrmypdf is installed, the built-in help which explains the command syntax and options can be accessed via:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf --help
|
||||
|
||||
Our `documentation is served on Read the Docs <https://ocrmypdf.readthedocs.io/en/latest/index.html>`_.
|
||||
|
||||
If you detect an issue, please:
|
||||
|
||||
- Check whether your issue is already known
|
||||
- If no problem report exists on github, please create one here:
|
||||
https://github.com/jbarlow83/OCRmyPDF/issues
|
||||
- Describe your problem thoroughly
|
||||
- Append the console output of the script when running the debug mode
|
||||
(``-v 1`` option)
|
||||
- If possible provide your input PDF file as well as the content of the
|
||||
temporary folder (using a file sharing service like Dropbox)
|
||||
|
||||
Press & Media
|
||||
-------------
|
||||
|
||||
- `c't 1-2014, page 59 <http://heise.de/-2279695>`_:
|
||||
Detailed presentation of OCRmyPDF v1.0 in the leading German IT
|
||||
magazine c't
|
||||
- `heise Open Source, 09/2014: Texterkennung mit
|
||||
OCRmyPDF <http://heise.de/-2356670>`_
|
||||
|
||||
Disclaimer
|
||||
----------
|
||||
|
||||
The software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||
CONDITIONS OF ANY KIND, either express or implied.
|
||||
@@ -1,435 +0,0 @@
|
||||
RELEASE NOTES
|
||||
=============
|
||||
|
||||
OCRmyPDF uses `semantic versioning <http://semver.org/>`_.
|
||||
|
||||
|
||||
v4.3:
|
||||
=====
|
||||
|
||||
- New feature ``--remove-background`` to detect and erase the background of color and grayscale images
|
||||
- Better documentation
|
||||
- Fixed an issue with PDFs that draw images when the raster stack depth is zero
|
||||
- ocrmypdf can now redirect its output to stdout for use in a shell pipeline
|
||||
|
||||
+ This does not improve performance since temporary files are still used for buffering
|
||||
+ Some output validation is disabled in this mode
|
||||
|
||||
v4.2.5:
|
||||
=======
|
||||
|
||||
- Fixed an issue (#100) with PDFs that omit the optional /BitsPerComponent parameter on images
|
||||
- Removed non-free file milk.pdf
|
||||
|
||||
|
||||
v4.2.4:
|
||||
=======
|
||||
|
||||
- Fixed an error (#90) caused by PDFs that use stencil masks properly
|
||||
- Fixed handling of PDFs that try to draw images or stencil masks without properly setting up the graphics state (such images are now ignored for the purposes of calculating DPI)
|
||||
|
||||
v4.2.3:
|
||||
=======
|
||||
|
||||
- Fixed an issue with PDFs that store page rotation (/Rotate) in an indirect object
|
||||
- Integrated a few fixes to simplify downstream packaging (Debian)
|
||||
|
||||
+ The test suite no longer assumes it is installed
|
||||
+ If running Linux, skip a test that passes Unicode on the command line
|
||||
|
||||
- Added a test case to check explicit masks and stencil masks
|
||||
- Added a test case for indirect objects and linearized PDFs
|
||||
- Deprecated the OCRmyPDF.sh shell script
|
||||
|
||||
|
||||
v4.2.2:
|
||||
=======
|
||||
|
||||
- Improvements to documentation
|
||||
|
||||
|
||||
v4.2.1:
|
||||
=======
|
||||
|
||||
- Fixed an issue where PDF pages that contained stencil masks would report an incorrect DPI and cause Ghostscript to abort
|
||||
- Implemented stdin streaming
|
||||
|
||||
|
||||
v4.2:
|
||||
=====
|
||||
|
||||
- ocrmypdf will now try to convert single image files to PDFs if they are provided as input (#15)
|
||||
|
||||
+ This is a basic convenience feature. It only supports a single image and always makes the image fill the whole page.
|
||||
+ For better control over image to PDF conversion, use ``img2pdf`` (one of ocrmypdf's dependencies)
|
||||
|
||||
- New argument ``--output-type {pdf|pdfa}`` allows disabling Ghostscript PDF/A generation
|
||||
|
||||
+ ``pdfa`` is the default, consistent with past behavior
|
||||
+ ``pdf`` provides a workaround for users concerned about the increase in file size from Ghostscript forcing JBIG2 images to CCITT and transcoding JPEGs
|
||||
+ ``pdf`` preserves as much as it can about the original file, including problems that PDF/A conversion fixes
|
||||
|
||||
- PDFs containing images with "non-square" pixel aspect ratios, such as 200x100 DPI, are now handled and converted properly (fixing a bug that caused to be cropped)
|
||||
- ``--force-ocr`` rasterizes pages even if they contain no images
|
||||
|
||||
+ supports users who want to use OCRmyPDF to reconstruct text information in PDFs with damaged Unicode maps (copy and paste text does not match displayed text)
|
||||
+ supports reinterpreting PDFs where text was rendered as curves for printing, and text needs to be recovered
|
||||
+ fixes issue #82
|
||||
|
||||
- Fixes an issue where, with certain settings, monochrome images in PDFs would be converted to 8-bit grayscale, increasing file size (#79)
|
||||
- Support for Ubuntu 12.04 LTS "precise" has been dropped in favor of (roughly) Ubuntu 14.04 LTS "trusty"
|
||||
|
||||
+ Some Ubuntu "PPAs" (backports) are needed to make it work
|
||||
|
||||
- Support for some older dependencies dropped
|
||||
|
||||
+ Ghostscript 9.15 or later is now required (available in Ubuntu trusty with backports)
|
||||
+ Tesseract 3.03 or later is now required (available in Ubuntu trusty)
|
||||
|
||||
- Ghostscript now runs in "safer" mode where possible
|
||||
|
||||
v4.1.4:
|
||||
=======
|
||||
|
||||
- Bug fix: monochrome images with an ICC profile attached were incorrectly converted to full color images if lossless reconstruction was not possible due to other settings; consequence was increased file size for these images
|
||||
|
||||
|
||||
v4.1.3:
|
||||
=======
|
||||
|
||||
- More helpful error message for PDFs with version 4 security handler
|
||||
- Update usage instructions for Windows/Docker users
|
||||
- Fix order of operations for matrix multiplication (no effect on most users)
|
||||
- Add a few leptonica wrapper functions (no effect on most users)
|
||||
|
||||
|
||||
v4.1.2:
|
||||
=======
|
||||
|
||||
- Replace IEC sRGB ICC profile with Debian's sRGB (from icc-profiles-free) which is more compatible with the MIT license
|
||||
- More helpful error message for an error related to certain types of malformed PDFs
|
||||
|
||||
|
||||
v4.1:
|
||||
=====
|
||||
|
||||
- ``--rotate-pages`` now only rotates pages when reasonably confidence in the orientation. This behavior can be adjusted with the new argument ``--rotate-pages-threshold``
|
||||
- Fixed problems in error checking if ``unpaper`` is uninstalled or missing at run-time
|
||||
- Fixed problems with "RethrownJobError" errors during error handling that suppressed the useful error messages
|
||||
|
||||
|
||||
v4.0.7:
|
||||
=======
|
||||
|
||||
- Minor correction to Ghostscript output settings
|
||||
|
||||
|
||||
v4.0.6:
|
||||
=======
|
||||
|
||||
- Update install instructions
|
||||
- Provide a sRGB profile instead of using Ghostscript's
|
||||
|
||||
|
||||
v4.0.5:
|
||||
=======
|
||||
|
||||
- Remove some verbose debug messages from v4.0.4
|
||||
- Fixed temporary that wasn't being deleted
|
||||
- DPI is now calculated correctly for cropped images, along with other image transformations
|
||||
- Inline images are now checked during DPI calculation instead of rejecting the image
|
||||
|
||||
v4.0.4:
|
||||
=======
|
||||
|
||||
Released with verbose debug message turned on. Do not use. Skip to v4.0.5.
|
||||
|
||||
|
||||
v4.0.3:
|
||||
=======
|
||||
|
||||
New features
|
||||
------------
|
||||
|
||||
- Page orientations detected are now reported in a summary comment
|
||||
|
||||
|
||||
Fixes
|
||||
-----
|
||||
|
||||
- Show stack trace if unexpected errors occur
|
||||
- Treat "too few characters" error message from Tesseract as a reason to skip that page rather than
|
||||
abort the file
|
||||
- Docker: fix blank JPEG2000 issue by insisting on Ghostscript versions that have this fixed
|
||||
|
||||
|
||||
v4.0.2:
|
||||
=======
|
||||
|
||||
Fixes
|
||||
-----
|
||||
|
||||
- Fixed compatibility with Tesseract 3.04.01 release, particularly its different way of outputting
|
||||
orientation information
|
||||
- Improved handling of Tesseract errors and crashes
|
||||
- Fixed use of chmod on Docker that broke most test cases
|
||||
|
||||
|
||||
v4.0.1:
|
||||
=======
|
||||
|
||||
Fixes
|
||||
-----
|
||||
|
||||
- Fixed a KeyError if tesseract fails to find page orientation information
|
||||
|
||||
|
||||
v4.0:
|
||||
=====
|
||||
|
||||
New features
|
||||
------------
|
||||
|
||||
- Automatic page rotation (``-r``) is now available. It uses ignores any prior rotation information
|
||||
on PDFs and sets rotation based on the dominant orientation of detectable text. This feature is
|
||||
fairly reliable but some false positives occur especially if there is not much text to work with. (#4)
|
||||
- Deskewing is now performed using Leptonica instead of unpaper. Leptonica is faster and more reliable
|
||||
at image deskewing than unpaper.
|
||||
|
||||
|
||||
Fixes
|
||||
-----
|
||||
|
||||
- Fixed an issue where lossless reconstruction could cause some pages to be appear incorrectly
|
||||
if the page was rotated by the user in Acrobat after being scanned (specifically if it a /Rotate tag)
|
||||
- Fixed an issue where lossless reconstruction could misalign the graphics layer with respect to
|
||||
text layer if the page had been cropped such that its origin is not (0, 0) (#49)
|
||||
|
||||
|
||||
Changes
|
||||
-------
|
||||
|
||||
- Logging output is now much easier to read
|
||||
- ``--deskew`` is now performed by Leptonica instead of unpaper (#25)
|
||||
- libffi is now required
|
||||
- Some changes were made to the Docker and Travis build environments to support libffi
|
||||
- ``--pdf-renderer=tesseract`` now displays a warning if the Tesseract version is less than 3.04.01,
|
||||
the planned release that will include fixes to an important OCR text rendering bug in Tesseract 3.04.00.
|
||||
You can also manually install ./share/sharp2.ttf on top of pdf.ttf in your Tesseract tessdata folder
|
||||
to correct the problem.
|
||||
|
||||
|
||||
v3.2.1:
|
||||
=======
|
||||
|
||||
Changes
|
||||
-------
|
||||
|
||||
- Fixed issue #47 "convert() got and unexpected keyword argument 'dpi'" by upgrading to img2pdf 0.2
|
||||
- Tweaked the Dockerfiles
|
||||
|
||||
|
||||
v3.2:
|
||||
=====
|
||||
|
||||
New features
|
||||
------------
|
||||
|
||||
- Lossless reconstruction: when possible, OCRmyPDF will inject text layers without
|
||||
otherwise manipulating the content and layout of a PDF page. For example, a PDF containing a mix
|
||||
of vector and raster content would see the vector content preserved. Images may still be transcoded
|
||||
during PDF/A conversion. (``--deskew`` and ``--clean-final`` disable this mode, necessarily.)
|
||||
- New argument ``--tesseract-pagesegmode`` allows you to pass page segmentation arguments to Tesseract OCR.
|
||||
This helps for two column text and other situations that confuse Tesseract.
|
||||
- Added a new "polyglot" version of the Docker image, that generates Tesseract with all languages packs installed,
|
||||
for the polyglots among us. It is much larger.
|
||||
|
||||
Changes
|
||||
-------
|
||||
|
||||
- JPEG transcoding quality is now 95 instead of the default 75. Bigger file sizes for less degradation.
|
||||
|
||||
|
||||
|
||||
v3.1.1:
|
||||
=======
|
||||
|
||||
Changes
|
||||
-------
|
||||
|
||||
- Fixed bug that caused incorrect page size and DPI calculations on documents with mixed page sizes
|
||||
|
||||
v3.1:
|
||||
=====
|
||||
|
||||
Changes
|
||||
-------
|
||||
|
||||
- Default output format is now PDF/A-2b instead of PDF/A-1b
|
||||
- Python 3.5 and OS X El Capitan are now supported platforms - no changes were
|
||||
needed to implement support
|
||||
- Improved some error messages related to missing input files
|
||||
- Fixed issue #20 - uppercase .PDF extension not accepted
|
||||
- Fixed an issue where OCRmyPDF failed to text that certain pages contained previously OCR'ed text,
|
||||
such as OCR text produced by Tesseract 3.04
|
||||
- Inserts /Creator tag into PDFs so that errors can be traced back to this project
|
||||
- Added new option ``--pdf-renderer=auto``, to let OCRmyPDF pick the best PDF renderer.
|
||||
Currently it always chooses the 'hocrtransform' renderer but that behavior may change.
|
||||
- Set up Travis CI automatic integration testing
|
||||
|
||||
v3.0:
|
||||
=====
|
||||
|
||||
New features
|
||||
------------
|
||||
|
||||
- Easier installation with a Docker container or Python's ``pip`` package manager
|
||||
- Eliminated many external dependencies, so it's easier to setup
|
||||
- Now installs ``ocrmypdf`` to ``/usr/local/bin`` or equivalent for system-wide
|
||||
access and easier typing
|
||||
- Improved command line syntax and usage help (``--help``)
|
||||
- Tesseract 3.03+ PDF page rendering can be used instead for better positioning
|
||||
of recognized text (``--pdf-renderer tesseract``)
|
||||
- PDF metadata (title, author, keywords) are now transferred to the
|
||||
output PDF
|
||||
- PDF metadata can also be set from the command line (``--title``, etc.)
|
||||
- Automatic repairs malformed input PDFs if possible
|
||||
- Added test cases to confirm everything is working
|
||||
- Added option to skip extremely large pages that take too long to OCR and are
|
||||
often not OCRable (e.g. large scanned maps or diagrams); other pages are still
|
||||
processed (``--skip-big``)
|
||||
- Added option to kill Tesseract OCR process if it seems to be taking too long on
|
||||
a page, while still processing other pages (``--tesseract-timeout``)
|
||||
- Less common colorspaces (CMYK, palette) are now supported by conversion to RGB
|
||||
- Multiple images on the same PDF page are now supported
|
||||
|
||||
Changes
|
||||
-------
|
||||
|
||||
- New, robust rewrite in Python 3.4+ with ruffus_ pipelines
|
||||
- Now uses Ghostscript 9.14's improved color conversion model to preserve PDF colors
|
||||
- OCR text is now rendered in the PDF as invisible text. Previous versions of OCRmyPDF
|
||||
incorrectly rendered visible text with an image on top.
|
||||
- All "tasks" in the pipeline can be executed in parallel on any
|
||||
available CPUs, increasing performance
|
||||
- The ``-o DPI`` argument has been phased out, in favor of ``--oversample DPI``, in
|
||||
case we need ``-o OUTPUTFILE`` in the future
|
||||
- Removed several dependencies, so it's easier to install. We no
|
||||
longer use:
|
||||
|
||||
- GNU parallel_
|
||||
- ImageMagick_
|
||||
- Python 2.7
|
||||
- Poppler
|
||||
- MuPDF_ tools
|
||||
- shell scripts
|
||||
- Java and JHOVE_
|
||||
- libxml2
|
||||
|
||||
- Some new external dependencies are required or optional, compared to v2.x:
|
||||
|
||||
- Ghostscript 9.14+
|
||||
- qpdf_ 5.0.0+
|
||||
- Unpaper_ 6.1 (optional)
|
||||
- some automatically managed Python packages
|
||||
|
||||
.. _ruffus: http://www.ruffus.org.uk/index.html
|
||||
.. _parallel: https://www.gnu.org/software/parallel/
|
||||
.. _ImageMagick: http://www.imagemagick.org/script/index.php
|
||||
.. _MuPDF: http://mupdf.com/docs/
|
||||
.. _qpdf: http://qpdf.sourceforge.net/
|
||||
.. _Unpaper: https://github.com/Flameeyes/unpaper
|
||||
.. _JHOVE: http://jhove.sourceforge.net/
|
||||
|
||||
Release candidates
|
||||
------------------
|
||||
|
||||
- rc9:
|
||||
|
||||
- fix issue #118: report error if ghostscript iccprofiles are missing
|
||||
- fixed another issue related to #111: PDF rasterized to palette file
|
||||
- add support image files with a palette
|
||||
- don't try to validate PDF file after an exception occurs
|
||||
|
||||
- rc8:
|
||||
|
||||
- fix issue #111: exception thrown if PDF is missing DocumentInfo dictionary
|
||||
|
||||
- rc7:
|
||||
|
||||
- fix error when installing direct from pip, "no such file 'requirements.txt'"
|
||||
|
||||
- rc6:
|
||||
|
||||
- dropped libxml2 (Python lxml) since Python 3's internal XML parser is sufficient
|
||||
- set up Docker container
|
||||
- fix Unicode errors if recognized text contains Unicode characters and system locale is not UTF-8
|
||||
|
||||
- rc5:
|
||||
|
||||
- dropped Java and JHOVE in favour of qpdf
|
||||
- improved command line error output
|
||||
- additional tests and bug fixes
|
||||
- tested on Ubuntu 14.04 LTS
|
||||
|
||||
- rc4:
|
||||
|
||||
- dropped MuPDF in favour of qpdf
|
||||
- fixed some installer issues and errors in installation instructions
|
||||
- improve performance: run Ghostscript with multithreaded rendering
|
||||
- improve performance: use multiple cores by default
|
||||
- bug fix: checking for wrong exception on process timeout
|
||||
|
||||
- rc3: skipping version number intentionally to avoid confusion with Tesseract
|
||||
- rc2: first release for public testing to test-PyPI, Github
|
||||
- rc1: testing release process
|
||||
|
||||
Compatibility notes
|
||||
-------------------
|
||||
|
||||
- ``./OCRmyPDF.sh`` script is still available for now
|
||||
- Stacking the verbosity option like ``-vvv`` is no longer supported
|
||||
|
||||
- The configuration file ``config.sh`` has been removed. Instead, you can
|
||||
feed a file to the arguments for common settings:
|
||||
|
||||
::
|
||||
|
||||
ocrmypdf input.pdf output.pdf @settings.txt
|
||||
|
||||
where ``settings.txt`` contains *one argument per line*, for example:
|
||||
|
||||
::
|
||||
|
||||
-l
|
||||
deu
|
||||
--author
|
||||
A. Merkel
|
||||
--pdf-renderer
|
||||
tesseract
|
||||
|
||||
|
||||
Fixes
|
||||
-----
|
||||
|
||||
- Handling of filenames containing spaces: fixed
|
||||
|
||||
Notes and known issues
|
||||
----------------------
|
||||
|
||||
- Some dependencies may work with lower versions than tested, so try
|
||||
overriding dependencies if they are "in the way" to see if they work.
|
||||
|
||||
- ``--pdf-renderer tesseract`` will output files with an incorrect page size in Tesseract 3.03,
|
||||
due to a bug in Tesseract.
|
||||
|
||||
- PDF files containing "inline images" are not supported and won't be for the 3.0 release. Scanned
|
||||
images almost never contain inline images.
|
||||
|
||||
|
||||
v2.2-stable (2014-09-29):
|
||||
=========================
|
||||
|
||||
OCRmyPDF versions 1 and 2 were implemented as shell scripts. OCRmyPDF 3.0+ is a fork that gradually replaced all shell scripts with Python while maintaining the existing command line arguments. No one is maintaining old versions.
|
||||
|
||||
For details on older versions, see the `final version of its release notes <https://github.com/fritz-hh/OCRmyPDF/blob/7fd3dbdf42ca53a619412ce8add7532c5e81a9d1/RELEASE_NOTES.md>`_.
|
||||
@@ -1,4 +0,0 @@
|
||||
check-manifest>=0.33
|
||||
setuptools-scm>=1.11.1
|
||||
twine>=1.8.1
|
||||
coverage>=4.2
|
||||
@@ -1,5 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
. /appenv/bin/activate
|
||||
cd /home/docker
|
||||
exec ocrmypdf "$@"
|
||||
@@ -0,0 +1,344 @@
|
||||
=================
|
||||
Advanced features
|
||||
=================
|
||||
|
||||
Control of unpaper
|
||||
==================
|
||||
|
||||
OCRmyPDF uses ``unpaper`` to provide the implementation of the
|
||||
``--clean`` and ``--clean-final`` arguments.
|
||||
`unpaper <https://github.com/Flameeyes/unpaper/blob/master/doc/basic-concepts.md>`__
|
||||
provides a variety of image processing filters to improve images.
|
||||
|
||||
By default, OCRmyPDF uses only ``unpaper`` arguments that were found to
|
||||
be safe to use on almost all files without having to inspect every page
|
||||
of the file afterwards. This is particularly true when only ``--clean``
|
||||
is used, since that instructs OCRmyPDF to only clean the image before
|
||||
OCR and not the final image.
|
||||
|
||||
However, if you wish to use the more aggressive options in ``unpaper``,
|
||||
you may use ``--unpaper-args '...'`` to override the OCRmyPDF's defaults
|
||||
and forward other arguments to unpaper. This option will forward
|
||||
arguments to ``unpaper`` without any knowledge of what that program
|
||||
considers to be valid arguments. The string of arguments must be quoted
|
||||
as shown in the examples below. No filename arguments may be included.
|
||||
OCRmyPDF will assume it can append input and output filename of
|
||||
intermediate images to the ``--unpaper-args`` string.
|
||||
|
||||
In this example, we tell ``unpaper`` to expect two pages of text on a
|
||||
sheet (image), such as occurs when two facing pages of a book are
|
||||
scanned. ``unpaper`` uses this information to deskew each independently
|
||||
and clean up the margins of both.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf --clean --clean-final --unpaper-args '--layout double' input.pdf output.pdf
|
||||
ocrmypdf --clean --clean-final --unpaper-args '--layout double --no-noisefilter' input.pdf output.pdf
|
||||
|
||||
.. warning::
|
||||
|
||||
Some ``unpaper`` features will reposition text within the image.
|
||||
``--clean-final`` is recommended to avoid this issue.
|
||||
|
||||
.. warning::
|
||||
|
||||
Some ``unpaper`` features cause multiple input or output files to be
|
||||
consumed or produced. OCRmyPDF requires ``unpaper`` to consume one
|
||||
file and produce one file. An deviation from that condition will
|
||||
result in errors.
|
||||
|
||||
.. note::
|
||||
|
||||
``unpaper`` uses uncompressed PBM/PGM/PPM files for its intermediate
|
||||
files. For large images or documents, it can take a lot of temporary
|
||||
disk space.
|
||||
|
||||
Control of OCR options
|
||||
======================
|
||||
|
||||
OCRmyPDF provides many features to control the behavior of the OCR
|
||||
engine, Tesseract.
|
||||
|
||||
When OCR is skipped
|
||||
-------------------
|
||||
|
||||
If a page in a PDF seems to have text, by default OCRmyPDF will exit
|
||||
without modifying the PDF. This is to ensure that PDFs that were
|
||||
previously OCRed or were "born digital" rather than scanned are not
|
||||
processed.
|
||||
|
||||
If ``--skip-text`` is issued, then no OCR will be performed on pages
|
||||
that already have text. The page will be copied to the output. This may
|
||||
be useful for documents that contain both "born digital" and scanned
|
||||
content, or to use OCRmyPDF to normalize and convert to PDF/A regardless
|
||||
of their contents.
|
||||
|
||||
If ``--redo-ocr`` is issued, then a detailed text analysis is performed.
|
||||
Text is categorized as either visible or invisible. Invisible text (OCR)
|
||||
is stripped out. Then an image of each page is created with visible text
|
||||
masked out. The page image is sent for OCR, and any additional text is
|
||||
inserted as OCR. If a file contains a mix of text and bitmap images that
|
||||
contain text, OCRmyPDF will locate the additional text in images without
|
||||
disrupting the existing text.
|
||||
|
||||
If ``--force-ocr`` is issued, then all pages will be rasterized to
|
||||
images, discarding any hidden OCR text, and rasterizing any printable
|
||||
text. This is useful for redoing OCR, for fixing OCR text with a damaged
|
||||
character map (text is selectable but not searchable), and destroying
|
||||
redacted information. Any forms and vector graphics will be rasterized
|
||||
as well.
|
||||
|
||||
Time and image size limits
|
||||
--------------------------
|
||||
|
||||
By default, OCRmyPDF permits tesseract to run for three minutes (180
|
||||
seconds) per page. This is usually more than enough time to find all
|
||||
text on a reasonably sized page with modern hardware.
|
||||
|
||||
If a page is skipped, it will be inserted without OCR. If preprocessing
|
||||
was requested, the preprocessed image layer will be inserted.
|
||||
|
||||
If you want to adjust the amount of time spent on OCR, change
|
||||
``--tesseract-timeout``. You can also automatically skip images that
|
||||
exceed a certain number of megapixels with ``--skip-big``. (A 300 DPI,
|
||||
8.5×11" page is 8.4 megapixels.)
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# Allow 300 seconds for OCR; skip any page larger than 50 megapixels
|
||||
ocrmypdf --tesseract-timeout 300 --skip-big 50 bigfile.pdf output.pdf
|
||||
|
||||
Overriding default tesseract
|
||||
----------------------------
|
||||
|
||||
OCRmyPDF checks the system ``PATH`` for the ``tesseract`` binary.
|
||||
|
||||
Some relevant environment variables that influence Tesseract's behavior
|
||||
include:
|
||||
|
||||
.. envvar:: TESSDATA_PREFIX
|
||||
|
||||
Overrides the path to Tesseract's data files. This can allow
|
||||
simultaneous installation of the "best" and "fast" training data
|
||||
sets. OCRmyPDF does not manage this environment variable.
|
||||
|
||||
.. envvar:: OMP_THREAD_LIMIT
|
||||
|
||||
Controls the number of threads Tesseract will use. OCRmyPDF will
|
||||
manage this environment variable if it is not already set.
|
||||
|
||||
For example, if you have a development build of Tesseract don't wish to
|
||||
use the system installation, you can launch OCRmyPDF as follows:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
env \
|
||||
PATH=/home/user/src/tesseract/api:$PATH \
|
||||
TESSDATA_PREFIX=/home/user/src/tesseract \
|
||||
ocrmypdf input.pdf output.pdf
|
||||
|
||||
In this example ``TESSDATA_PREFIX`` is required to redirect Tesseract to
|
||||
an alternate folder for its "tessdata" files.
|
||||
|
||||
Overriding other support programs
|
||||
---------------------------------
|
||||
|
||||
In addition to tesseract, OCRmyPDF uses the following external binaries:
|
||||
|
||||
- ``gs`` (Ghostscript)
|
||||
- ``unpaper``
|
||||
- ``pngquant``
|
||||
- ``jbig2``
|
||||
|
||||
In each case OCRmyPDF will search the ``PATH`` environment variable to
|
||||
locate the binaries.
|
||||
|
||||
Changing tesseract configuration variables
|
||||
------------------------------------------
|
||||
|
||||
You can override tesseract's default `control
|
||||
parameters <https://github.com/tesseract-ocr/tesseract/wiki/ControlParams>`__
|
||||
with a configuration file.
|
||||
|
||||
As an example, this configuration will disable Tesseract's dictionary
|
||||
for current language. Normally the dictionary is helpful for
|
||||
interpolating words that are unclear, but it may interfere with OCR if
|
||||
the document does not contain many words (for example, a list of part
|
||||
numbers).
|
||||
|
||||
Create a file named "no-dict.cfg" with these contents:
|
||||
|
||||
::
|
||||
|
||||
load_system_dawg 0
|
||||
language_model_penalty_non_dict_word 0
|
||||
language_model_penalty_non_freq_dict_word 0
|
||||
|
||||
then run ocrmypdf as follows (along with any other desired arguments):
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf --tesseract-config no-dict.cfg input.pdf output.pdf
|
||||
|
||||
.. warning::
|
||||
|
||||
Some combinations of control parameters will break Tesseract or break
|
||||
assumptions that OCRmyPDF makes about Tesseract's output.
|
||||
|
||||
Changing the PDF renderer
|
||||
=========================
|
||||
|
||||
rasterizing
|
||||
Converting a PDF to an image for display.
|
||||
|
||||
rendering
|
||||
Creating a new PDF from other data (such as an existing PDF).
|
||||
|
||||
OCRmyPDF has these PDF renderers: ``sandwich`` and ``hocr``. The
|
||||
renderer may be selected using ``--pdf-renderer``. The default is
|
||||
``auto`` which lets OCRmyPDF select the renderer to use. Currently,
|
||||
``auto`` always selects ``sandwich``.
|
||||
|
||||
The ``sandwich`` renderer
|
||||
-------------------------
|
||||
|
||||
The ``sandwich`` renderer uses Tesseract's new text-only PDF feature,
|
||||
which produces a PDF page that lays out the OCR in invisible text. This
|
||||
page is then "sandwiched" onto the original PDF page, allowing lossless
|
||||
application of OCR even to PDF pages that contain other vector objects.
|
||||
|
||||
Currently this is the best renderer for most uses, however it is
|
||||
implemented in Tesseract so OCRmyPDF cannot influence it. Currently some
|
||||
problematic PDF viewers like Mozilla PDF.js and macOS Preview have
|
||||
problems with segmenting its text output, and
|
||||
mightrunseveralwordstogether.
|
||||
|
||||
When image preprocessing features like ``--deskew`` are used, the
|
||||
original PDF will be rendered as a full page and the OCR layer will be
|
||||
placed on top.
|
||||
|
||||
The ``hocr`` renderer
|
||||
---------------------
|
||||
|
||||
The ``hocr`` renderer works with older versions of Tesseract. The image
|
||||
layer is copied from the original PDF page if possible, avoiding
|
||||
potentially lossy transcoding or loss of other PDF information. If
|
||||
preprocessing is specified, then the image layer is a new PDF.
|
||||
|
||||
Unlike ``sandwich`` this renderer is implemented within OCRmyPDF; anyone
|
||||
looking to customize how OCR is presented should look here. A major
|
||||
disadvantage of this renderer is it not capable of correctly handling
|
||||
text outside the Latin alphabet (specifically, it supports the ISO 8859-1
|
||||
character). Pull requests to improve the situation are welcome.
|
||||
|
||||
Currently, this renderer has the best compatibility with Mozilla's
|
||||
PDF.js viewer.
|
||||
|
||||
This works in all versions of Tesseract.
|
||||
|
||||
The ``tesseract`` renderer
|
||||
--------------------------
|
||||
|
||||
The ``tesseract`` renderer was removed. OCRmyPDF's new approach to text
|
||||
layer grafting makes it functionally equivalent to ``sandwich``.
|
||||
|
||||
Return code policy
|
||||
==================
|
||||
|
||||
OCRmyPDF writes all messages to ``stderr``. ``stdout`` is reserved for
|
||||
piping output files. ``stdin`` is reserved for piping input files.
|
||||
|
||||
The return codes generated by the OCRmyPDF are considered part of the
|
||||
stable user interface. They may be imported from
|
||||
``ocrmypdf.exceptions``.
|
||||
|
||||
.. list-table:: Return codes
|
||||
:widths: 5 35 60
|
||||
:header-rows: 1
|
||||
|
||||
* - Code
|
||||
- Name
|
||||
- Interpretation
|
||||
* - 0
|
||||
- ``ExitCode.ok``
|
||||
- Everything worked as expected.
|
||||
* - 1
|
||||
- ``ExitCode.bad_args``
|
||||
- Invalid arguments, exited with an error.
|
||||
* - 2
|
||||
- ``ExitCode.input_file``
|
||||
- The input file does not seem to be a valid PDF.
|
||||
* - 3
|
||||
- ``ExitCode.missing_dependency``
|
||||
- An external program required by OCRmyPDF is missing.
|
||||
* - 4
|
||||
- ``ExitCode.invalid_output_pdf``
|
||||
- An output file was created, but it does not seem to be a valid PDF. The file will be available.
|
||||
* - 5
|
||||
- ``ExitCode.file_access_error``
|
||||
- The user running OCRmyPDF does not have sufficient permissions to read the input file and write the output file.
|
||||
* - 6
|
||||
- ``ExitCode.already_done_ocr``
|
||||
- The file already appears to contain text so it may not need OCR. See output message.
|
||||
* - 7
|
||||
- ``ExitCode.child_process_error``
|
||||
- An error occurred in an external program (child process) and OCRmyPDF cannot continue.
|
||||
* - 8
|
||||
- ``ExitCode.encrypted_pdf``
|
||||
- The input PDF is encrypted. OCRmyPDF does not read encrypted PDFs. Use another program such as ``qpdf`` to remove encryption.
|
||||
* - 9
|
||||
- ``ExitCode.invalid_config``
|
||||
- A custom configuration file was forwarded to Tesseract using ``--tesseract-config``, and Tesseract rejected this file.
|
||||
* - 10
|
||||
- ``ExitCode.pdfa_conversion_failed``
|
||||
- A valid PDF was created, PDF/A conversion failed. The file will be available.
|
||||
* - 15
|
||||
- ``ExitCode.other_error``
|
||||
- Some other error occurred.
|
||||
* - 130
|
||||
- ``ExitCode.ctrl_c``
|
||||
- The program was interrupted by pressing Ctrl+C.
|
||||
|
||||
|
||||
Debugging the intermediate files
|
||||
================================
|
||||
|
||||
OCRmyPDF normally saves its intermediate results to a temporary folder
|
||||
and deletes this folder when it exits, whether it succeeded or failed.
|
||||
|
||||
If the ``-k`` argument is issued on the command line, OCRmyPDF will keep
|
||||
the temporary folder and print the location, whether it succeeded or
|
||||
failed (provided the Python interpreter did not crash). An example
|
||||
message is:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
Temporary working files retained at:
|
||||
/tmp/ocrmypdf.io.u20wpz07
|
||||
|
||||
The organization of this folder is an implementation detail and subject
|
||||
to change between releases. However the general organization is that
|
||||
working files on a per page basis have the page number as a prefix
|
||||
(starting with page 1), an infix indicates the processing stage, and a
|
||||
suffix indicates the file type. Some important files include:
|
||||
|
||||
- ``_rasterize.png`` - what the input page looks like
|
||||
- ``_ocr.png`` - the file that is sent to Tesseract for OCR; depending
|
||||
on arguments this may differ from the presentation image
|
||||
- ``_pp_deskew.png`` - the image, after deskewing
|
||||
- ``_pp_clean.png`` - the image, after cleaning with unpaper
|
||||
- ``_ocr_tess.pdf`` - the OCR file; appears as a blank page with invisible
|
||||
text embedded
|
||||
- ``_ocr_tess.txt`` - the OCR text (not necessarily all text on the page,
|
||||
if the page is mixed format)
|
||||
- ``fix_docinfo.pdf`` - a temporary file created to fix the PDF DocumentInfo
|
||||
data structure
|
||||
- ``graft_layers.pdf`` - the rendered PDF with OCR layers grafted on
|
||||
- ``pdfa.pdf`` - ``graft_layers.pdf`` after conversion to PDF/A
|
||||
- ``pdfa.ps`` - a PostScript file used by Ghostscript for PDF/A conversion
|
||||
- ``optimize.pdf`` - the PDF generated before optimization
|
||||
- ``optimize.out.pdf`` - the PDF generated by optimization
|
||||
- ``origin`` - the input file
|
||||
- ``origin.pdf`` - the input file or the input image converted to PDF
|
||||
- ``images/*`` - images extracted during the optimization process; here
|
||||
the prefix indicates a PDF object ID not a page number
|
||||
@@ -0,0 +1,117 @@
|
||||
======================
|
||||
Using the OCRmyPDF API
|
||||
======================
|
||||
|
||||
OCRmyPDF originated as a command line program and continues to have this
|
||||
legacy, but parts of it can be imported and used in other Python
|
||||
applications.
|
||||
|
||||
Some applications may want to consider running ocrmypdf from a
|
||||
subprocess call anyway, as this provides isolation of its activities.
|
||||
|
||||
Example
|
||||
=======
|
||||
|
||||
OCRmyPDF provides one high-level function to run its main engine from an
|
||||
application. The parameters are symmetric to the command line arguments
|
||||
and largely have the same functions.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import ocrmypdf
|
||||
|
||||
if __name__ == '__main__': # To ensure correct behavior on Windows and macOS
|
||||
ocrmypdf.ocr('input.pdf', 'output.pdf', deskew=True)
|
||||
|
||||
With some exceptions, all of the command line arguments are available
|
||||
and may be passed as equivalent keywords.
|
||||
|
||||
A few differences are that ``verbose`` and ``quiet`` are not available.
|
||||
Instead, output should be managed by configuring logging.
|
||||
|
||||
Parent process requirements
|
||||
---------------------------
|
||||
|
||||
The :func:`ocrmypdf.ocr` function runs OCRmyPDF similar to command line
|
||||
execution. To do this, it will:
|
||||
|
||||
- create a monitoring thread
|
||||
- create worker processes (on Linux, forking itself; on Windows and macOS, by
|
||||
spawning)
|
||||
- manage the signal flags of its worker processes
|
||||
- execute other subprocesses (forking and executing other programs)
|
||||
|
||||
The Python process that calls :func:`ocrmypdf.ocr()` must be sufficiently
|
||||
privileged to perform these actions.
|
||||
|
||||
There currently is no option to manage how jobs are scheduled other
|
||||
than the argument ``jobs=`` which will limit the number of worker
|
||||
processes.
|
||||
|
||||
Creating a child process to call :func:`ocrmypdf.ocr()` is suggested. That
|
||||
way your application will survive and remain interactive even if
|
||||
OCRmyPDF fails for any reason.
|
||||
|
||||
Programs that call :func:`ocrmypdf.ocr()` should also install a SIGBUS signal
|
||||
handler (except on Windows), to raise an exception if access to a memory
|
||||
mapped file fails. OCRmyPDF may use memory mapping.
|
||||
|
||||
:func:`ocrmypdf.ocr()` will take a threading lock to prevent multiple runs of itself
|
||||
in the same Python interpreter process. This is not thread-safe, because of how
|
||||
OCRmyPDF's plugins and Python's library import system work. If you need to parallelize
|
||||
OCRmyPDF, use processes.
|
||||
|
||||
.. warning::
|
||||
|
||||
On Windows and macOS, the script that calls :func:`ocrmypdf.ocr()` must be
|
||||
protected by an "ifmain" guard (``if __name__ == '__main__'``). If you do
|
||||
not take at least one of these steps, process semantics will prevent
|
||||
OCRmyPDF from working correctly.
|
||||
|
||||
Logging
|
||||
-------
|
||||
|
||||
OCRmyPDF will log under loggers named ``ocrmypdf``. In addition, it
|
||||
imports ``pdfminer`` and ``PIL``, both of which post log messages under
|
||||
those logging namespaces.
|
||||
|
||||
You can configure the logging as desired for your application or call
|
||||
:func:`ocrmypdf.configure_logging` to configure logging the same way
|
||||
OCRmyPDF itself does. The command line parameters such as ``--quiet``
|
||||
and ``--verbose`` have no equivalents in the API; you must use the
|
||||
provided configuration function or do configuration in a way that suits
|
||||
your use case.
|
||||
|
||||
Progress monitoring
|
||||
-------------------
|
||||
|
||||
OCRmyPDF uses the ``tqdm`` package to implement its progress bars.
|
||||
:func:`ocrmypdf.configure_logging` will set up logging output to
|
||||
``sys.stderr`` in a way that is compatible with the display of the
|
||||
progress bar. Use ``ocrmypdf.ocr(...progress_bar=False)`` to disable
|
||||
the progress bar.
|
||||
|
||||
Exceptions
|
||||
----------
|
||||
|
||||
OCRmyPDF may throw standard Python exceptions, ``ocrmypdf.exceptions.*``
|
||||
exceptions, some exceptions related to multiprocessing, and
|
||||
:exc:`KeyboardInterrupt`. The parent process should provide an exception
|
||||
handler. OCRmyPDF will clean up its temporary files and worker processes
|
||||
automatically when an exception occurs.
|
||||
|
||||
Programs that call OCRmyPDF should consider trapping KeyboardInterrupt
|
||||
so that they allow OCR to terminate with the whole program terminating.
|
||||
|
||||
When OCRmyPDF succeeds conditionally, it returns an integer exit code.
|
||||
|
||||
Reference
|
||||
---------
|
||||
|
||||
.. autofunction:: ocrmypdf.ocr
|
||||
|
||||
.. autoclass:: ocrmypdf.Verbosity
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
.. autofunction:: ocrmypdf.configure_logging
|
||||
@@ -0,0 +1,55 @@
|
||||
=============
|
||||
API Reference
|
||||
=============
|
||||
|
||||
This page summarizes the rest of the public API. Generally speaking this
|
||||
should mainly of interest to plugin developers.
|
||||
|
||||
ocrmypdf
|
||||
========
|
||||
|
||||
.. autoclass:: ocrmypdf.PageContext
|
||||
:members:
|
||||
|
||||
.. autoclass:: ocrmypdf.PdfContext
|
||||
:members:
|
||||
|
||||
ocrmypdf.exceptions
|
||||
===================
|
||||
|
||||
.. automodule:: ocrmypdf.exceptions
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
ocrmypdf.helpers
|
||||
================
|
||||
|
||||
.. automodule:: ocrmypdf.helpers
|
||||
:members:
|
||||
:noindex: deprecated
|
||||
|
||||
.. autodecorator:: deprecated
|
||||
|
||||
ocrmypdf.hocrtransform
|
||||
======================
|
||||
|
||||
.. automodule:: ocrmypdf.hocrtransform
|
||||
:members:
|
||||
|
||||
ocrmypdf.pdfa
|
||||
=============
|
||||
|
||||
.. automodule:: ocrmypdf.pdfa
|
||||
:members:
|
||||
|
||||
ocrmypdf.quality
|
||||
================
|
||||
|
||||
.. automodule:: ocrmypdf.quality
|
||||
:members:
|
||||
|
||||
ocrmypdf.subprocess
|
||||
===================
|
||||
|
||||
.. automodule:: ocrmypdf.subprocess
|
||||
:members:
|
||||
@@ -0,0 +1,217 @@
|
||||
================
|
||||
Batch processing
|
||||
================
|
||||
|
||||
This article provides information about running OCRmyPDF on multiple
|
||||
files or configuring it as a service triggered by file system events.
|
||||
|
||||
Batch jobs
|
||||
==========
|
||||
|
||||
Consider using the excellent `GNU
|
||||
Parallel <https://www.gnu.org/software/parallel/>`__ to apply OCRmyPDF
|
||||
to multiple files at once.
|
||||
|
||||
Both ``parallel`` and ``ocrmypdf`` will try to use all available
|
||||
processors. To maximize parallelism without overloading your system with
|
||||
processes, consider using ``parallel -j 2`` to limit parallel to running
|
||||
two jobs at once.
|
||||
|
||||
This command will run all ocrmypdf all files named ``*.pdf`` in the
|
||||
current directory and write them to the previous created ``output/``
|
||||
folder. It will not search subdirectories.
|
||||
|
||||
The ``--tag`` argument tells parallel to print the filename as a prefix
|
||||
whenever a message is printed, so that one can trace any errors to the
|
||||
file that produced them.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
parallel --tag -j 2 ocrmypdf '{}' 'output/{}' ::: *.pdf
|
||||
|
||||
OCRmyPDF automatically repairs PDFs before parsing and gathering
|
||||
information from them.
|
||||
|
||||
Directory trees
|
||||
===============
|
||||
|
||||
This will walk through a directory tree and run OCR on all files in
|
||||
place, printing the output in a way that makes
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
find . -printf '%p' -name '*.pdf' -exec ocrmypdf '{}' '{}' \;
|
||||
|
||||
Alternatively, with a docker container (mounts a volume to the container
|
||||
where the PDFs are stored):
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
find . -printf '%p' -name '*.pdf' -exec docker run --rm -v <host dir>:<container dir> jbarlow83/ocrmypdf '<container dir>/{}' '<container dir>/{}' \;
|
||||
|
||||
This only runs one ``ocrmypdf`` process at a time. This variation uses
|
||||
``find`` to create a directory list and ``parallel`` to parallelize runs
|
||||
of ``ocrmypdf``, again updating files in place.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
find . -name '*.pdf' | parallel --tag -j 2 ocrmypdf '{}' '{}'
|
||||
|
||||
In a Windows batch file, use
|
||||
|
||||
.. code-block:: bat
|
||||
|
||||
for /r %%f in (*.pdf) do ocrmypdf %%f %%f
|
||||
|
||||
Sample script
|
||||
-------------
|
||||
|
||||
This user contributed script also provides an example of batch
|
||||
processing.
|
||||
|
||||
.. literalinclude:: ../misc/batch.py
|
||||
:caption: misc/batch.py
|
||||
|
||||
Synology DiskStations
|
||||
---------------------
|
||||
|
||||
Synology DiskStations (Network Attached Storage devices) can run the
|
||||
Docker image of OCRmyPDF if the Synology `Docker
|
||||
package <https://www.synology.com/en-global/dsm/packages/Docker>`__ is
|
||||
installed. Attached is a script to address particular quirks of using
|
||||
OCRmyPDF on one of these devices.
|
||||
|
||||
This is only possible for x86-based Synology products. Some Synology
|
||||
products use ARM or Power processors and do not support Docker. Further
|
||||
adjustments might be needed to deal with the Synology's relatively
|
||||
limited CPU and RAM.
|
||||
|
||||
.. literalinclude:: ../misc/synology.py
|
||||
:caption: misc/synology.py - Sample script for Synology DiskStations
|
||||
|
||||
Huge batch jobs
|
||||
---------------
|
||||
|
||||
If you have thousands of files to work with, contact the author.
|
||||
Consulting work related to OCRmyPDF helps fund this open source project
|
||||
and all inquiries are appreciated.
|
||||
|
||||
Hot (watched) folders
|
||||
=====================
|
||||
|
||||
Watched folders with watcher.py
|
||||
-------------------------------
|
||||
|
||||
OCRmyPDF has a folder watcher called watcher.py, which is currently included in source
|
||||
distributions but not part of the main program. It may be used natively or may run
|
||||
in a Docker container. Native instances tend to give better performance. watcher.py
|
||||
works on all platforms.
|
||||
|
||||
Users may need to customize the script to meet their requirements.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip3 install ocrmypdf[watcher]
|
||||
|
||||
env OCR_INPUT_DIRECTORY=/mnt/input-pdfs \
|
||||
OCR_OUTPUT_DIRECTORY=/mnt/output-pdfs \
|
||||
OCR_OUTPUT_DIRECTORY_YEAR_MONTH=1 \
|
||||
python3 watcher.py
|
||||
|
||||
.. csv-table:: watcher.py environment variables
|
||||
:header: "Environment variable", "Description"
|
||||
:widths: 50, 50
|
||||
|
||||
"OCR_INPUT_DIRECTORY", "Set input directory to monitor (recursive)"
|
||||
"OCR_OUTPUT_DIRECTORY", "Set output directory (should not be under input)"
|
||||
"OCR_ON_SUCCESS_DELETE", "This will delete the input file if the exit code is 0 (OK)"
|
||||
"OCR_OUTPUT_DIRECTORY_YEAR_MONTH", "This will place files in the output in ``{output}/{year}/{month}/{filename}``"
|
||||
"OCR_DESKEW", "Apply deskew to crooked input PDFs"
|
||||
"OCR_JSON_SETTINGS", "A JSON string specifying any other arguments for ``ocrmypdf.ocr``, e.g. ``'OCR_JSON_SETTINGS={""rotate_pages"": true}'``."
|
||||
"OCR_POLL_NEW_FILE_SECONDS", "Polling interval"
|
||||
"OCR_LOGLEVEL", "Level of log messages to report"
|
||||
|
||||
One could configure a networked scanner or scanning computer to drop files in the
|
||||
watched folder.
|
||||
|
||||
Watched folders with Docker
|
||||
---------------------------
|
||||
|
||||
The watcher service is included in the OCRmyPDF Docker image. To run it:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker run \
|
||||
-v <path to files to convert>:/input \
|
||||
-v <path to store results>:/output \
|
||||
-e OCR_OUTPUT_DIRECTORY_YEAR_MONTH=1 \
|
||||
-e OCR_ON_SUCCESS_DELETE=1 \
|
||||
-e OCR_DESKEW=1 \
|
||||
-e PYTHONUNBUFFERED=1 \
|
||||
-it --entrypoint python3 \
|
||||
jbarlow83/ocrmypdf \
|
||||
watcher.py
|
||||
|
||||
This service will watch for a file that matches ``/input/\*.pdf`` and will
|
||||
convert it to a OCRed PDF in ``/output/``. The parameters to this image are:
|
||||
|
||||
.. csv-table:: watcher.py parameters for Docker
|
||||
:header: "Parameter", "Description"
|
||||
:widths: 50, 50
|
||||
|
||||
"``-v <path to files to convert>:/input``", "Files placed in this location will be OCRed"
|
||||
"``-v <path to store results>:/output``", "This is where OCRed files will be stored"
|
||||
"``-e OCR_OUTPUT_DIRECTORY_YEAR_MONTH=1``", "Define environment variable OCR_OUTPUT_DIRECTORY_YEAR_MONTH=1"
|
||||
"``-e OCR_ON_SUCCESS_DELETE=1``", "Define environment variable"
|
||||
"``-e OCR_DESKEW=1``", "Define environment variable"
|
||||
"``-e PYTHONBUFFERED=1``", "This will force STDOUT to be unbuffered and allow you to see messages in docker logs"
|
||||
|
||||
This service relies on polling to check for changes to the filesystem. It
|
||||
may not be suitable for some environments, such as filesystems shared on a
|
||||
slow network.
|
||||
|
||||
A configuration manager such as Docker Compose could be used to ensure that the
|
||||
service is always available.
|
||||
|
||||
.. literalinclude:: ../misc/docker-compose.example.yml
|
||||
:language: yaml
|
||||
:caption: misc/docker-compose.example.yml
|
||||
|
||||
Caveats
|
||||
-------
|
||||
|
||||
- ``watchmedo`` may not work properly on a networked file system,
|
||||
depending on the capabilities of the file system client and server.
|
||||
- This simple recipe does not filter for the type of file system event,
|
||||
so file copies, deletes and moves, and directory operations, will all
|
||||
be sent to ocrmypdf, producing errors in several cases. Disable your
|
||||
watched folder if you are doing anything other than copying files to
|
||||
it.
|
||||
- If the source and destination directory are the same, watchmedo may
|
||||
create an infinite loop.
|
||||
- On BSD, FreeBSD and older versions of macOS, you may need to increase
|
||||
the number of file descriptors to monitor more files, using
|
||||
``ulimit -n 1024`` to watch a folder of up to 1024 files.
|
||||
|
||||
Alternatives
|
||||
------------
|
||||
|
||||
- On Linux, `systemd user services <https://wiki.archlinux.org/index.php/Systemd/User>`__
|
||||
can be configured to automatically perform OCR on a collection of files.
|
||||
|
||||
- `Watchman <https://facebook.github.io/watchman/>`__ is a more
|
||||
powerful alternative to ``watchmedo``.
|
||||
|
||||
macOS Automator
|
||||
===============
|
||||
|
||||
You can use the Automator app with macOS, to create a Workflow or Quick
|
||||
Action. Use a *Run Shell Script* action in your workflow. In the context
|
||||
of Automator, the ``PATH`` may be set differently your Terminal's
|
||||
``PATH``; you may need to explicitly set the PATH to include
|
||||
``ocrmypdf``. The following example may serve as a starting point:
|
||||
|
||||
.. figure:: images/macos-workflow.png
|
||||
:alt: Example macOS Automator workflow
|
||||
|
||||
You may customize the command sent to ocrmypdf.
|
||||
@@ -1,5 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# ocrmypdf documentation build configuration file, created by
|
||||
# sphinx-quickstart on Sun Sep 4 14:29:43 2016.
|
||||
@@ -21,6 +20,8 @@
|
||||
# import sys
|
||||
# sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
"""isort:skip_file"""
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
@@ -31,9 +32,18 @@
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
'sphinx.ext.mathjax',
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinx.ext.intersphinx',
|
||||
'sphinx.ext.autosummary',
|
||||
'sphinx.ext.napoleon',
|
||||
'sphinx_issues',
|
||||
]
|
||||
|
||||
# Extension settings
|
||||
intersphinx_mapping = {'https://docs.python.org/': None}
|
||||
napoleon_use_rtype = False
|
||||
issues_github_path = "jbarlow83/OCRmyPDF"
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
@@ -52,7 +62,9 @@ master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = 'ocrmypdf'
|
||||
copyright = '2016, James R. Barlow'
|
||||
copyright = (
|
||||
'2021, James R. Barlow. Licensed under Creative Commons Attribution-ShareAlike 4.0.'
|
||||
)
|
||||
author = 'James R. Barlow'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
@@ -60,13 +72,35 @@ author = 'James R. Barlow'
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
from ocrmypdf import VERSION as OCRMYPDF_VERSION
|
||||
|
||||
_version_parts = OCRMYPDF_VERSION.split('.')
|
||||
import os
|
||||
|
||||
on_rtd = os.environ.get('READTHEDOCS') == 'True'
|
||||
|
||||
if on_rtd:
|
||||
# Help ReadTheDocs avoid having to install any binary extension modules
|
||||
import sys
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
class Mock(MagicMock):
|
||||
@classmethod
|
||||
def __getattr__(cls, name):
|
||||
return MagicMock()
|
||||
|
||||
MOCK_MODULES = [
|
||||
'pikepdf',
|
||||
'pikepdf.models',
|
||||
'pikepdf.models.metadata',
|
||||
'ocrmypdf.leptonica',
|
||||
]
|
||||
sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES)
|
||||
|
||||
from importlib_metadata import version as package_version
|
||||
|
||||
version = '.'.join(_version_parts[0:2])
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = OCRMYPDF_VERSION
|
||||
release = package_version('ocrmypdf')
|
||||
version = '.'.join(release.split('.')[:2])
|
||||
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
@@ -135,9 +169,7 @@ html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#
|
||||
html_theme_options = {
|
||||
'display_version': False
|
||||
}
|
||||
html_theme_options = {'display_version': False}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
# html_theme_path = []
|
||||
@@ -154,7 +186,7 @@ html_theme_options = {
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
#
|
||||
# html_logo = None
|
||||
# html_logo = "images/logo.svg" # looks bad
|
||||
|
||||
# The name of an image file (relative to this directory) to use as a favicon of
|
||||
# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
@@ -249,30 +281,26 @@ htmlhelp_basename = 'ocrmypdfdoc'
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#
|
||||
# 'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#
|
||||
# 'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#
|
||||
# 'preamble': '',
|
||||
|
||||
# Latex figure (float) alignment
|
||||
#
|
||||
# 'figure_align': 'htbp',
|
||||
latex_elements = { # type: ignore
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#
|
||||
# 'papersize': 'letterpaper',
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#
|
||||
# 'pointsize': '10pt',
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#
|
||||
# 'preamble': '',
|
||||
# Latex figure (float) alignment
|
||||
#
|
||||
# 'figure_align': 'htbp',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(master_doc, 'ocrmypdf.tex', 'ocrmypdf Documentation',
|
||||
'James R. Barlow', 'manual'),
|
||||
(master_doc, 'ocrmypdf.tex', 'ocrmypdf Documentation', 'James R. Barlow', 'manual')
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
@@ -312,10 +340,7 @@ latex_documents = [
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(master_doc, 'ocrmypdf', 'ocrmypdf Documentation',
|
||||
[author], 1)
|
||||
]
|
||||
man_pages = [(master_doc, 'ocrmypdf', 'ocrmypdf Documentation', [author], 1)]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#
|
||||
@@ -328,9 +353,15 @@ man_pages = [
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(master_doc, 'ocrmypdf', 'ocrmypdf Documentation',
|
||||
author, 'ocrmypdf', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
(
|
||||
master_doc,
|
||||
'ocrmypdf',
|
||||
'ocrmypdf Documentation',
|
||||
author,
|
||||
'ocrmypdf',
|
||||
'One line description of project.',
|
||||
'Miscellaneous',
|
||||
)
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
=======================
|
||||
Contributing guidelines
|
||||
=======================
|
||||
|
||||
Contributions are welcome!
|
||||
|
||||
Big changes
|
||||
===========
|
||||
|
||||
Please open a new issue to discuss or propose a major change. Not only is it fun
|
||||
to discuss big ideas, but we might save each other's time too. Perhaps some of the
|
||||
work you're contemplating is already half-done in a development branch.
|
||||
|
||||
Code style
|
||||
==========
|
||||
|
||||
We use PEP8, ``black`` for code formatting and ``isort`` for import sorting. The
|
||||
settings for these programs are in ``pyproject.toml`` and ``setup.cfg``. Pull
|
||||
requests should follow the style guide. One difference we use from "black" style
|
||||
is that strings shown to the user are always in double quotes (``"``) and strings
|
||||
for internal uses are in single quotes (``'``).
|
||||
|
||||
Tests
|
||||
=====
|
||||
|
||||
New features should come with tests that confirm their correctness.
|
||||
|
||||
New Python dependencies
|
||||
=======================
|
||||
|
||||
If you are proposing a change that will require a new Python dependency, we
|
||||
prefer dependencies that are already packaged by Debian or Red Hat. This makes
|
||||
life much easier for our downstream package maintainers.
|
||||
|
||||
Python dependencies must also be license-compatible. GPLv3 or AGPLv3 are likely
|
||||
incompatible with the project's license, but LGPLv3 is compatible.
|
||||
|
||||
New non-Python dependencies
|
||||
===========================
|
||||
|
||||
OCRmyPDF uses several external programs (Tesseract, Ghostscript and others) for
|
||||
its functionality. In general we prefer to avoid adding new external programs.
|
||||
|
||||
Style guide: Is it OCRmyPDF or ocrmypdf?
|
||||
========================================
|
||||
|
||||
The program/project is OCRmyPDF and the name of the executable or library is ocrmypdf.
|
||||
|
||||
Known ports/packagers
|
||||
=====================
|
||||
|
||||
OCRmyPDF has been ported to many platforms already. If you are interesting in
|
||||
porting to a new platform, check with
|
||||
`Repology <https://repology.org/projects/?search=ocrmypdf>`__ to see the status
|
||||
of that platform.
|
||||
|
||||
Packager maintainers, please ensure that the command line completion scripts in
|
||||
``misc/`` are installed.
|
||||
|
||||
Copyright and license
|
||||
=====================
|
||||
|
||||
For contributions over 10 lines of code, please include your name to list of
|
||||
copyright holders for that file. The core program is licensed under MPL-2.0,
|
||||
test files and documentation under CC-BY-SA 4.0, and miscellaneous files under
|
||||
MIT. Please contribute code only that you wrote and you have the permission to
|
||||
contribute or license to us.
|
||||
@@ -1,139 +1,361 @@
|
||||
========
|
||||
Cookbook
|
||||
========
|
||||
|
||||
Basic examples
|
||||
--------------
|
||||
==============
|
||||
|
||||
Help!
|
||||
"""""
|
||||
-----
|
||||
|
||||
ocrmypdf has built-in help.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf --help
|
||||
|
||||
ocrmypdf --help
|
||||
|
||||
Add an OCR layer and convert to PDF/A
|
||||
"""""""""""""""""""""""""""""""""""""
|
||||
-------------------------------------
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf input.pdf output.pdf
|
||||
ocrmypdf input.pdf output.pdf
|
||||
|
||||
Add an OCR layer and output a standard PDF
|
||||
""""""""""""""""""""""""""""""""""""""""""
|
||||
------------------------------------------
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf --output-type pdf input.pdf output.pdf
|
||||
ocrmypdf --output-type pdf input.pdf output.pdf
|
||||
|
||||
Create a PDF/A with all color and grayscale images converted to JPEG
|
||||
--------------------------------------------------------------------
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf --output-type pdfa --pdfa-image-compression jpeg input.pdf output.pdf
|
||||
|
||||
Modify a file in place
|
||||
""""""""""""""""""""""
|
||||
----------------------
|
||||
|
||||
The file will only be overwritten if OCRmyPDF is successful.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf myfile.pdf myfile.pdf
|
||||
ocrmypdf myfile.pdf myfile.pdf
|
||||
|
||||
Correct page rotation
|
||||
"""""""""""""""""""""
|
||||
---------------------
|
||||
|
||||
OCR will attempt to automatic correct the rotation of each page. This can help fix a scanning job that contains a mix of landscape and portrait pages.
|
||||
OCR will attempt to automatic correct the rotation of each page. This
|
||||
can help fix a scanning job that contains a mix of landscape and
|
||||
portrait pages.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf --rotate-pages myfile.pdf myfile.pdf
|
||||
ocrmypdf --rotate-pages myfile.pdf myfile.pdf
|
||||
|
||||
You can increase (decrease) the parameter ``--rotate-pages-threshold`` to make page rotation more (less) aggressive.
|
||||
You can increase (decrease) the parameter ``--rotate-pages-threshold``
|
||||
to make page rotation more (less) aggressive. The threshold number is the ratio
|
||||
of how confidence the OCR engine is that the document image should be changed,
|
||||
compared to kept the same. The default value is quite conservative; on some files
|
||||
it may not attempt rotations at all unless it is very confident that the current
|
||||
rotation is wrong. A lower value of ``2.0`` will produce more rotations, and
|
||||
more false positives. Run with ``-v1`` to see the confidence level for each
|
||||
page to see if there may be a better value for your files.
|
||||
|
||||
If the page is "just a little off horizontal", like a crooked picture,
|
||||
then you want ``--deskew``. ``--rotate-pages`` is for when the cardinal
|
||||
angle is wrong.
|
||||
|
||||
OCR languages other than English
|
||||
--------------------------------
|
||||
|
||||
OCRmyPDF assumes the document is in English unless told otherwise. OCR
|
||||
quality may be poor if the wrong language is used.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf -l fra LeParisien.pdf LeParisien.pdf
|
||||
ocrmypdf -l eng+fra Bilingual-English-French.pdf Bilingual-English-French.pdf
|
||||
|
||||
Language packs must be installed for all languages specified. See
|
||||
:ref:`Installing additional language packs <lang-packs>`.
|
||||
|
||||
Unfortunately, the Tesseract OCR engine has no ability to detect the
|
||||
language when it is unknown.
|
||||
|
||||
Produce PDF and text file containing OCR text
|
||||
---------------------------------------------
|
||||
|
||||
This produces a file named "output.pdf" and a companion text file named
|
||||
"output.txt".
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf --sidecar output.txt input.pdf output.pdf
|
||||
|
||||
.. note::
|
||||
|
||||
The sidecar file contains the **OCR text** found by OCRmyPDF. If the document
|
||||
contains pages that already have text, that text will not appear in the
|
||||
sidecar. If the option ``--pages`` is used, only those pages on which OCR
|
||||
was performed will be included in the sidecar. If certain pages were skipped
|
||||
because of options like ``--skip-big`` or ``--tesseract-timeout``, those pages
|
||||
will not be in the sidecar.
|
||||
|
||||
To extract all text from a PDF, whether generated from OCR or otherwise,
|
||||
use a program like Poppler's ``pdftotext`` or ``pdfgrep``.
|
||||
|
||||
OCR images, not PDFs
|
||||
--------------------
|
||||
|
||||
Use a program like `img2pdf <https://gitlab.mister-muffin.de/josch/img2pdf>`_ to convert your images to PDFs, and then pipe the resutls to run ocrmypdf:
|
||||
Option: use Tesseract
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
If you are starting with images, you can just use Tesseract directly to
|
||||
convert images to PDFs:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
img2pdf my-images*.jpg | ocrmypdf - myfile.pdf
|
||||
|
||||
If given a single image as input, OCRmyPDF will try converting it to a PDF on its own. This feature may be removed at some point, because OCRmyPDF does not specialize in converting images to PDFs.
|
||||
|
||||
You can also use Tesseract 3.04+ directly to convert single page images or multi-page TIFFs to PDF:
|
||||
tesseract my-image.jpg output-prefix pdf
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
tesseract my-image.jpg output-prefix pdf
|
||||
# When there are multiple images
|
||||
tesseract text-file-containing-list-of-image-filenames.txt output-prefix pdf
|
||||
|
||||
Tesseract's PDF output is quite good – OCRmyPDF uses it internally, in
|
||||
some cases. However, OCRmyPDF has many features not available in
|
||||
Tesseract like image processing, metadata control, and PDF/A generation.
|
||||
|
||||
Option: use img2pdf
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
You can also use a program like
|
||||
`img2pdf <https://gitlab.mister-muffin.de/josch/img2pdf>`__ to convert
|
||||
your images to PDFs, and then pipe the results to run ocrmypdf. The
|
||||
``-`` tells ocrmypdf to read standard input.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
img2pdf my-images*.jpg | ocrmypdf - myfile.pdf
|
||||
|
||||
``img2pdf`` is recommended because it does an excellent job at
|
||||
generating PDFs without transcoding images.
|
||||
|
||||
Option: use OCRmyPDF (single images only)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
For convenience, OCRmyPDF can also convert single images to PDFs on its
|
||||
own. If the resolution (dots per inch, DPI) of an image is not set or is
|
||||
incorrect, it can be overridden with ``--image-dpi``. (As 1 inch is 2.54
|
||||
cm, 1 dpi = 0.39 dpcm).
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf --image-dpi 300 image.png myfile.pdf
|
||||
|
||||
If you have multiple images, you must use ``img2pdf`` to convert the
|
||||
images to PDF.
|
||||
|
||||
Not recommended
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
We caution against using ImageMagick or Ghostscript to convert images to
|
||||
PDF, since they may transcode images or produce downsampled images,
|
||||
sometimes without warning.
|
||||
|
||||
Image processing
|
||||
----------------
|
||||
================
|
||||
|
||||
OCRmyPDF perform some image processing on each page of a PDF, if desired. The same processing is applied to each page. It is suggested that the user review files after image processing as these commands might remove desirable content, especially from poor quality scans.
|
||||
OCRmyPDF perform some image processing on each page of a PDF, if
|
||||
desired. The same processing is applied to each page. It is suggested
|
||||
that the user review files after image processing as these commands
|
||||
might remove desirable content, especially from poor quality scans.
|
||||
|
||||
* ``--rotate-pages`` attempts to determine the correct orientation for each page and rotates the page if necessary.
|
||||
- ``--rotate-pages`` attempts to determine the correct orientation for
|
||||
each page and rotates the page if necessary.
|
||||
- ``--remove-background`` attempts to detect and remove a noisy
|
||||
background from grayscale or color images. Monochrome images are
|
||||
ignored. This should not be used on documents that contain color
|
||||
photos as it may remove them.
|
||||
- ``--deskew`` will correct pages were scanned at a skewed angle by
|
||||
rotating them back into place. Skew determination and correction is
|
||||
performed using `Postl's variance of line
|
||||
sums <http://www.leptonica.org/skew-measurement.html>`__ algorithm as
|
||||
implemented in `Leptonica <http://www.leptonica.org/index.html>`__.
|
||||
- ``--clean`` uses
|
||||
`unpaper <https://www.flameeyes.eu/projects/unpaper>`__ to clean up
|
||||
pages before OCR, but does not alter the final output. This makes it
|
||||
less likely that OCR will try to find text in background noise.
|
||||
- ``--clean-final`` uses unpaper to clean up pages before OCR and
|
||||
inserts the page into the final output. You will want to review each
|
||||
page to ensure that unpaper did not remove something important.
|
||||
|
||||
* ``--remove-background`` attempts to detect and remove a noisy background from grayscale or color images. Monochrome images are ignored. This should not be used on documents that contain color photos as it may remove them.
|
||||
.. note::
|
||||
|
||||
* ``--deskew`` will correct pages were scanned at a skewed angle by rotating them back into place. Skew determination and correction is performed using `Postl's variance of line sums <http://www.leptonica.com/skew-measurement.html>`_ algorithm as implemented in `Leptonica <http://www.leptonica.com/index.html>`_.
|
||||
|
||||
* ``--clean`` uses `unpaper <https://www.flameeyes.eu/projects/unpaper>`_ to clean up pages before OCR, but does not alter the final output. This makes it less likely that OCR will try to find text in background noise.
|
||||
In many cases image processing will rasterize PDF pages as images,
|
||||
potentially losing quality.
|
||||
|
||||
* ``--clean-final`` uses unpaper to clean up pages before OCR and inserts the page into the final output. You will want to review each page to ensure that unpaper did not remove something important.
|
||||
.. warning::
|
||||
|
||||
``--clean-final`` and ``-remove-background`` may leave undesirable
|
||||
visual artifacts in some images where their algorithms have
|
||||
shortcomings. Files should be visually reviewed after using these
|
||||
options.
|
||||
|
||||
OCR and correct document skew (crooked scan)
|
||||
""""""""""""""""""""""""""""""""""""""""""""
|
||||
Example: OCR and correct document skew (crooked scan)
|
||||
-----------------------------------------------------
|
||||
|
||||
Deskew:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf --deskew input.pdf output.pdf
|
||||
ocrmypdf --deskew input.pdf output.pdf
|
||||
|
||||
|
||||
Hot (watched) folders
|
||||
---------------------
|
||||
|
||||
To set up a "hot folder" that will trigger an OCR operation for every file inserted, use a program like Python `watchdog <https://pypi.python.org/pypi/watchdog>`_ (supports all major OS).
|
||||
Image processing commands can be combined. The order in which options
|
||||
are given does not matter. OCRmyPDF always applies the steps of the
|
||||
image processing pipeline in the same order (rotate, remove background,
|
||||
deskew, clean).
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip install watchdog
|
||||
ocrmypdf --deskew --clean --rotate-pages input.pdf output.pdf
|
||||
|
||||
watchdog installs the command line program ``watchmedo``, which can be told to run ``ocrmypdf`` on any .pdf added to the current directory (``.``) and place the result in the previously created ``out/`` folder.
|
||||
Don't actually OCR my PDF
|
||||
=========================
|
||||
|
||||
If you set ``--tesseract-timeout 0`` OCRmyPDF will apply its image
|
||||
processing without performing OCR, if all you want to is to apply image
|
||||
processing or PDF/A conversion.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
cd hot-folder
|
||||
mkdir out
|
||||
watchmedo shell-command \
|
||||
--patterns="*.pdf" \
|
||||
--command='ocrmypdf "${watch_src_path}" "out/${watch_src_path}" ' \
|
||||
. # don't forget the final dot
|
||||
ocrmypdf --tesseract-timeout=0 --remove-background input.pdf output.pdf
|
||||
|
||||
For more complex behavior you can write a Python script around to use the watchdog API.
|
||||
Optimize images without performing OCR
|
||||
--------------------------------------
|
||||
|
||||
On file servers, you could configure watchmedo as a system service so it will run all the time.
|
||||
|
||||
|
||||
Caveats
|
||||
"""""""
|
||||
|
||||
* ``watchmedo`` may not work properly on a networked file system, depending on the capabilities of the file system client and server.
|
||||
* This simple recipe does not filter for the type of file system event, so file copies, deletes and moves, and directory operations, will all be sent to ocrmypdf, producing errors in several cases. Disable your watched folder if you are doing anything other than copying files to it.
|
||||
|
||||
|
||||
Batch jobs
|
||||
----------
|
||||
|
||||
Consider using the excellent `GNU Parallel <https://www.gnu.org/software/parallel/>`_ to apply OCRmyPDF to multiple files at once.
|
||||
|
||||
Both ``parallel`` and ``ocrmypdf`` will try to use all available processors. To maximize parallelism without overloading your system with processes, consider using ``parallel -j 2`` to limit parallel to running two jobs at once.
|
||||
|
||||
This command will run all ocrmypdf all files named ``*.pdf`` in the current directory and write them to the previous created ``output/`` folder.
|
||||
You can also optimize all images without performing any OCR:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
parallel -j 2 ocrmypdf '{}' 'output/{}' ::: *.pdf
|
||||
ocrmypdf --tesseract-timeout=0 --optimize 3 --skip-text input.pdf output.pdf
|
||||
|
||||
If you have thousands of files to work with, contact the author.
|
||||
|
||||
Perform OCR only certain pages
|
||||
------------------------------
|
||||
|
||||
You can ask OCRmyPDF to only apply OCR to certain pages.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf --pages 2,3,13-17 input.pdf output.pdf
|
||||
|
||||
Hyphens denote a range of pages and commas separate page numbers. If you prefer
|
||||
to use spaces, quote all of the page numbers: ``--pages '2, 3, 5, 7'``.
|
||||
|
||||
OCRmyPDF will warn if your list of page numbers contains duplicates or
|
||||
overlap pages. OCRmyPDF does not currently account for document page numbers,
|
||||
such as an introduction section of a book that uses Roman numerals. It simply
|
||||
counts the number of virtual pieces of paper since the start.
|
||||
|
||||
Regardless of the argument to ``--pages``, OCRmyPDF will optimize all pages in
|
||||
the file and convert it to PDF/A, unless you disable those options. In this
|
||||
example, we want to OCR only the title and otherwise change the PDF as little
|
||||
as possible:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf --pages 1 --output-type pdf --optimize 0 input.pdf output.pdf
|
||||
|
||||
Redo existing OCR
|
||||
=================
|
||||
|
||||
To redo OCR on a file OCRed with other OCR software or a previous
|
||||
version of OCRmyPDF and/or Tesseract, you may use the ``--redo-ocr``
|
||||
argument. (Normally, OCRmyPDF will exit with an error if asked to modify
|
||||
a file with OCR.)
|
||||
|
||||
This may be helpful for users who want to take advantage of accuracy
|
||||
improvements in Tesseract 4.0 for files they previously OCRed with an
|
||||
earlier version of Tesseract and OCRmyPDF.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf --redo-ocr input.pdf output.pdf
|
||||
|
||||
This method will replace OCR without rasterizing, reducing quality or
|
||||
removing vector content. If a file contains a mix of pure digital text
|
||||
and OCR, digital text will be ignored and OCR will be replaced. As such
|
||||
this mode is incompatible with image processing options, since they
|
||||
alter the appearance of the file.
|
||||
|
||||
In some cases, existing OCR cannot be detected or replaced. Files
|
||||
produced by OCRmyPDF v2.2 or earlier, for example, are internally
|
||||
represented as having visible text with an opaque image drawn on top.
|
||||
This situation cannot be detected.
|
||||
|
||||
If ``--redo-ocr`` does not work, you can use ``--force-ocr``, which will
|
||||
force rasterization of all pages, potentially reducing quality or losing
|
||||
vector content.
|
||||
|
||||
Improving OCR quality
|
||||
=====================
|
||||
|
||||
The `Image processing <#image-processing>`__ features can improve OCR
|
||||
quality.
|
||||
|
||||
Rotating pages and deskewing helps to ensure that the page orientation
|
||||
is correct before OCR begins. Removing the background and/or cleaning
|
||||
the page can also improve results. The ``--oversample DPI`` argument can
|
||||
be specified to resample images to higher resolution before attempting
|
||||
OCR; this can improve results as well.
|
||||
|
||||
OCR quality will suffer if the resolution of input images is not correct
|
||||
(since the range of pixel sizes that will be checked for possible fonts
|
||||
will also be incorrect).
|
||||
|
||||
PDF optimization
|
||||
================
|
||||
|
||||
By default OCRmyPDF will attempt to perform lossless optimizations on
|
||||
the images inside PDFs after OCR is complete. Optimization is performed
|
||||
even if no OCR text is found.
|
||||
|
||||
The ``--optimize N`` (short form ``-O``) argument controls optimization,
|
||||
where ``N`` ranges from 0 to 3 inclusive, analogous to the optimization
|
||||
levels in the GCC compiler.
|
||||
|
||||
.. list-table::
|
||||
:widths: auto
|
||||
:header-rows: 1
|
||||
|
||||
* - Level
|
||||
- Comments
|
||||
* - ``--optimize 0``
|
||||
- Disables optimization.
|
||||
* - ``--optimize 1``
|
||||
- Enables lossless optimizations, such as transcoding images to more
|
||||
efficient formats. Also compress other uncompressed objects in the
|
||||
PDF and enables the more efficient "object streams" within the PDF.
|
||||
* - ``--optimize 2``
|
||||
- All of the above, and enables lossy optimizations and color quantization.
|
||||
* - ``--optimize 3``
|
||||
- All of the above, and enables more aggressive optimizations and targets lower image quality.
|
||||
|
||||
Optimization is improved when a JBIG2 encoder is available and when
|
||||
``pngquant`` is installed. If either of these components are missing,
|
||||
then some types of images cannot be optimized.
|
||||
|
||||
The types of optimization available may expand over time. By default,
|
||||
OCRmyPDF compresses data streams inside PDFs, and will change
|
||||
inefficient compression modes to more modern versions. A program like
|
||||
``qpdf`` can be used to change encodings, e.g. to inspect the internals
|
||||
fo a PDF.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf --optimize 3 in.pdf out.pdf # Make it small
|
||||
|
||||
Some users may consider enabling lossy JBIG2. See: :ref:`jbig2-lossy`.
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
.. _docker:
|
||||
|
||||
=====================
|
||||
OCRmyPDF Docker image
|
||||
=====================
|
||||
|
||||
OCRmyPDF is also available in a Docker image that packages recent
|
||||
versions of all dependencies.
|
||||
|
||||
For users who already have Docker installed this may be an easy and
|
||||
convenient option. However, it is less performant than a system
|
||||
installation and may require Docker engine configuration.
|
||||
|
||||
OCRmyPDF needs a generous amount of RAM, CPU cores, temporary storage
|
||||
space, whether running in a Docker container or on its own. It may be
|
||||
necessary to ensure the container is provisioned with additional
|
||||
resources.
|
||||
|
||||
.. _docker-install:
|
||||
|
||||
Installing the Docker image
|
||||
===========================
|
||||
|
||||
If you have `Docker <https://docs.docker.com/>`__ installed on your
|
||||
system, you can install a Docker image of the latest release.
|
||||
|
||||
If you can run this command successfully, your system is ready to download and
|
||||
execute the image:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker run hello-world
|
||||
|
||||
The recommended OCRmyPDF Docker image is currently named ``ocrmypdf``:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker pull jbarlow83/ocrmypdf
|
||||
|
||||
|
||||
OCRmyPDF will use all available CPU cores. By default, the VirtualBox
|
||||
machine instance on Windows and macOS has only a single CPU core
|
||||
enabled. Use the VirtualBox Manager to determine the name of your Docker
|
||||
engine host, and then follow these optional steps to enable multiple
|
||||
CPUs:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# Optional step for Mac OS X users
|
||||
docker-machine stop "yourVM"
|
||||
VBoxManage modifyvm "yourVM" --cpus 2 # or whatever number of core is desired
|
||||
docker-machine start "yourVM"
|
||||
eval $(docker-machine env "yourVM")
|
||||
|
||||
See the Docker documentation for
|
||||
`adjusting memory and CPU on other platforms <https://docs.docker.com/config/containers/resource_constraints/>`__.
|
||||
|
||||
Using the Docker image on the command line
|
||||
==========================================
|
||||
|
||||
**Unlike typical Docker containers**, in this section the OCRmyPDF Docker
|
||||
container is emphemeral – it runs for one OCR job and terminates, just like a
|
||||
command line program. We are using Docker to deliver an application (as opposed
|
||||
to the more conventional case, where a Docker container runs as a server).
|
||||
|
||||
To start a Docker container (instance of the image):
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker tag jbarlow83/ocrmypdf ocrmypdf
|
||||
docker run --rm -i ocrmypdf (... all other arguments here...) - -
|
||||
|
||||
For convenience, create a shell alias to hide the Docker command. It is
|
||||
easier to send the input file as stdin and read the output from
|
||||
stdout – **this avoids the messy permission issues with Docker entirely**.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
alias docker_ocrmypdf='docker run --rm -i ocrmypdf'
|
||||
docker_ocrmypdf --version # runs docker version
|
||||
docker_ocrmypdf - - <input.pdf >output.pdf
|
||||
|
||||
Or in the wonderful `fish shell <https://fishshell.com/>`__:
|
||||
|
||||
.. code-block:: fish
|
||||
|
||||
alias docker_ocrmypdf 'docker run --rm ocrmypdf'
|
||||
funcsave docker_ocrmypdf
|
||||
|
||||
Alternately, you could mount the local current working directory as a
|
||||
Docker volume:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
alias docker_ocrmypdf='docker run --rm -i --user "$(id -u):$(id -g)" --workdir /data -v "$PWD:/data" ocrmypdf'
|
||||
docker_ocrmypdf /data/input.pdf /data/output.pdf
|
||||
|
||||
.. _docker-lang-packs:
|
||||
|
||||
Adding languages to the Docker image
|
||||
====================================
|
||||
|
||||
By default the Docker image includes English, German, Simplified Chinese,
|
||||
French, Portuguese and Spanish, the most popular languages for OCRmyPDF
|
||||
users based on feedback. You may add other languages by creating a new
|
||||
Dockerfile based on the public one.
|
||||
|
||||
.. code-block:: dockerfile
|
||||
|
||||
FROM jbarlow83/ocrmypdf
|
||||
|
||||
# Example: add Italian
|
||||
RUN apt install tesseract-ocr-ita
|
||||
|
||||
To install language packs (training data) such as the
|
||||
`tessdata_best <https://github.com/tesseract-ocr/tessdata_best>`_ suite or
|
||||
custom data, you first need to determine the version of Tesseract data files, which
|
||||
may differ from the Tesseract program version. Use this command to determine the data
|
||||
file version:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker run -i --rm --entrypoint /bin/ls jbarlow83/ocrmypdf /usr/share/tesseract-ocr
|
||||
|
||||
As of 2021, the data file version is probably ``4.00``.
|
||||
|
||||
You can then add new data with either a Dockerfile:
|
||||
|
||||
.. code-block:: dockerfile
|
||||
|
||||
FROM jbarlow83/ocrmypdf
|
||||
|
||||
# Example: add a tessdata_best file
|
||||
COPY chi_tra_vert.traineddata /usr/share/tesseract-ocr/<data version>/tessdata/
|
||||
|
||||
Alternately, you can copy training data into a Docker container as follows:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker cp mycustomtraining.traineddata name_of_container:/usr/share/tesseract-ocr/<tesseract version>/tessdata/
|
||||
|
||||
Executing the test suite
|
||||
========================
|
||||
|
||||
The OCRmyPDF test suite is installed with image. To run it:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker run --entrypoint python3 jbarlow83/ocrmypdf -m pytest
|
||||
|
||||
Accessing the shell
|
||||
===================
|
||||
|
||||
To use the bash shell in the Docker image:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker run -it --entrypoint bash jbarlow83/ocrmypdf
|
||||
|
||||
Using the OCRmyPDF web service wrapper
|
||||
======================================
|
||||
|
||||
The OCRmyPDF Docker image includes an example, barebones HTTP web
|
||||
service. The webservice may be launched as follows:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker run --entrypoint python3 -p 5000:5000 jbarlow83/ocrmypdf webservice.py
|
||||
|
||||
This will configure the machine to listen on port 5000. On Linux machines
|
||||
this is port 5000 of localhost. On macOS or Windows machines running
|
||||
Docker, this is port 5000 of the virtual machine that runs your Docker
|
||||
images. You can find its IP address using the command ``docker-machine ip``.
|
||||
|
||||
Unlike command line usage this program will open a socket and wait for
|
||||
connections.
|
||||
|
||||
.. warning::
|
||||
|
||||
The OCRmyPDF web service wrapper is intended for demonstration or
|
||||
development. It provides no security, no authentication, no
|
||||
protection against denial of service attacks, and no load balancing.
|
||||
The default Flask WSGI server is used, which is intended for
|
||||
development only. The server is single-threaded and so can respond to
|
||||
only one client at a time. While running OCR, it cannot respond to
|
||||
any other clients.
|
||||
|
||||
Clients must keep their open connection while waiting for OCR to
|
||||
complete. This may entail setting a long timeout; this interface is more
|
||||
useful for internal HTTP API calls.
|
||||
|
||||
Unlike the rest of OCRmyPDF, this web service is licensed under the
|
||||
Affero GPLv3 (AGPLv3) since Ghostscript is also licensed in this way.
|
||||
|
||||
In addition to the above, please read our
|
||||
:ref:`general remarks on using OCRmyPDF as a service <ocr-service>`.
|
||||
@@ -1,33 +1,53 @@
|
||||
=====================
|
||||
Common error messages
|
||||
=====================
|
||||
|
||||
Page already has text
|
||||
---------------------
|
||||
=====================
|
||||
|
||||
.. code::
|
||||
.. code-block::
|
||||
|
||||
ERROR - 1: page already has text! – aborting (use --force-ocr to force OCR)
|
||||
ERROR - 1: page already has text! – aborting (use --force-ocr to force OCR)
|
||||
|
||||
You ran ocrmypdf on a file that already contains printable text or a hidden OCR text layer (it can't quite tell the difference). You probably don't want to do this, because the file is already searchable.
|
||||
You ran ocrmypdf on a file that already contains printable text or a
|
||||
hidden OCR text layer (it can't quite tell the difference). You probably
|
||||
don't want to do this, because the file is already searchable.
|
||||
|
||||
As the error message suggests, your options are:
|
||||
|
||||
- ``ocrmypdf --force-ocr`` to :ref:`rasterize <raster-vector>` all vector content and run OCR on the images. This is useful if a previous OCR program failed, or if the document contains a text watermark.
|
||||
|
||||
- ``ocrmypdf --skip-text`` to skip OCR and other processing on any pages that contain text. Text pages will be copied into the output PDF without modification.
|
||||
- ``ocrmypdf --force-ocr`` to :ref:`rasterize <raster-vector>` all
|
||||
vector content and run OCR on the images. This is useful if a
|
||||
previous OCR program failed, or if the document contains a text
|
||||
watermark.
|
||||
- ``ocrmypdf --skip-text`` to skip OCR and other processing on any
|
||||
pages that contain text. Text pages will be copied into the output
|
||||
PDF without modification.
|
||||
- ``ocrmypdf --redo-ocr`` to scan the file for any existing OCR
|
||||
(non-printing text), remove it, and do OCR again. This is one way
|
||||
to take advantage of improvements in OCR accuracy. Printable vector
|
||||
text is excluded from OCR, so this can be used on files that contain
|
||||
a mix of digital and scanned files.
|
||||
|
||||
|
||||
Input file 'filename' is not a valid PDF
|
||||
----------------------------------------
|
||||
========================================
|
||||
|
||||
OCRmyPDF passes files through qpdf, a program that fixes errors in PDFs, before it tries to work on them. In most cases this happens because the PDF is corrupt and
|
||||
truncated (incomplete file copying) and not much can be done.
|
||||
OCRmyPDF checks files with pikepdf, a library that in turn uses libqpdf to fixes
|
||||
errors in PDFs, before it tries to work on them. In most cases this happens
|
||||
because the PDF is corrupt and truncated (incomplete file copying) and not much
|
||||
can be done.
|
||||
|
||||
You can try rewriting the file with Ghostscript or pdftk:
|
||||
You can try rewriting the file with Ghostscript:
|
||||
|
||||
- ``gs -o output.pdf -dSAFER -sDEVICE=pdfwrite input.pdf``
|
||||
.. code-block:: bash
|
||||
|
||||
- ``pdftk input.pdf cat output output.pdf``
|
||||
gs -o output.pdf -dSAFER -sDEVICE=pdfwrite input.pdf
|
||||
|
||||
Sometimes Acrobat can repair PDFs with its `Preflight tool <https://helpx.adobe.com/acrobat/using/correcting-problem-areas-preflight-tool.html>`_.
|
||||
``pdftk`` can also rewrite PDFs:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pdftk input.pdf cat output output.pdf
|
||||
|
||||
Sometimes Acrobat can repair PDFs with its `Preflight
|
||||
tool <https://helpx.adobe.com/acrobat/using/correcting-problem-areas-preflight-tool.html>`__.
|
||||
|
||||
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,233 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:serif="http://www.serif.com/"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="503"
|
||||
height="503"
|
||||
viewBox="0 0 503 503"
|
||||
version="1.1"
|
||||
xml:space="preserve"
|
||||
style="clip-rule:evenodd;fill-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5"
|
||||
id="svg270"
|
||||
sodipodi:docname="logo-square.svg"
|
||||
inkscape:export-filename="/home/jb/src/ocrmypdf/docs/images/logo-square.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
inkscape:version="1.0.2 (e86c870879, 2021-01-15)"><metadata
|
||||
id="metadata276"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs274" /><sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="2396"
|
||||
inkscape:window-height="1691"
|
||||
id="namedview272"
|
||||
showgrid="false"
|
||||
lock-margins="false"
|
||||
inkscape:zoom="2.0079523"
|
||||
inkscape:cx="251.5"
|
||||
inkscape:cy="193.18317"
|
||||
inkscape:window-x="26"
|
||||
inkscape:window-y="23"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg270" />
|
||||
<g
|
||||
id="svg"
|
||||
transform="matrix(0.965977,0,0,0.807602,0,138.43572)">
|
||||
<rect
|
||||
x="0"
|
||||
y="0"
|
||||
width="520"
|
||||
height="280"
|
||||
style="fill:#ffffff"
|
||||
id="rect188" />
|
||||
<g
|
||||
transform="matrix(1.03522,0,0,1.23823,-69.7528,-83.422)"
|
||||
id="g267">
|
||||
<g
|
||||
transform="translate(243.977,20.0703)"
|
||||
id="g218">
|
||||
<g
|
||||
id="Page">
|
||||
<g
|
||||
transform="matrix(0.961773,0,0,1.05962,6.19811,-3.01071)"
|
||||
id="g192">
|
||||
<path
|
||||
d="m 328.5,97.682 c 0,-1.217 -0.517,-2.386 -1.444,-3.264 -7.03,-6.66 -37.614,-35.638 -44.828,-42.474 -0.977,-0.925 -2.327,-1.448 -3.738,-1.448 -13.997,0 -90.407,0 -111.151,0 -2.871,0 -5.198,2.113 -5.198,4.718 0,27.837 0,170.351 0,198.186 0,2.605 2.327,4.717 5.197,4.717 24.904,0 131.821,0 156.2,0 2.74,0 4.962,-2.016 4.962,-4.504 0,-24.345 0,-139.717 0,-155.931 z"
|
||||
style="fill:#fdfdfd;stroke:#333333;stroke-width:3.95px"
|
||||
id="path190" />
|
||||
</g>
|
||||
<g
|
||||
id="Dog-ear"
|
||||
serif:id="Dog ear"
|
||||
transform="translate(-4,2)">
|
||||
<path
|
||||
d="m 277.072,48.496 v 45.352 c 0,1.324 0.526,2.593 1.462,3.529 0.936,0.936 2.205,1.462 3.529,1.462 12.485,0 44.078,0 44.078,0"
|
||||
style="fill:#f5f5f5;stroke:#333333;stroke-width:4px"
|
||||
id="path194" />
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
transform="translate(-29.6816,-0.395178)"
|
||||
id="g216">
|
||||
<g
|
||||
transform="matrix(1.00243,0,0,1.11818,-144.72,-8.80181)"
|
||||
id="g200">
|
||||
<path
|
||||
d="m 465.73,119.654 c 0,-2.049 -1.856,-3.713 -4.142,-3.713 H 310.259 c -2.286,0 -4.142,1.664 -4.142,3.713 v 63.454 c 0,2.049 1.856,3.713 4.142,3.713 h 151.329 c 2.286,0 4.142,-1.664 4.142,-3.713 z"
|
||||
style="fill:#f80000;stroke:#ffffff;stroke-width:3.77px"
|
||||
id="path198" />
|
||||
</g>
|
||||
<g
|
||||
transform="matrix(1.24571,0,0,1.35864,116.812,84.3924)"
|
||||
id="g214">
|
||||
<g
|
||||
transform="matrix(64,0,0,64,42.1437,77.6203)"
|
||||
id="g204">
|
||||
<path
|
||||
d="m 0.084,0 v -0.68 h 0.213 c 0.074,0 0.137,0.017 0.19,0.05 0.053,0.034 0.079,0.09 0.079,0.168 0,0.077 -0.028,0.134 -0.085,0.17 -0.057,0.037 -0.121,0.055 -0.193,0.055 H 0.213 V 0 Z m 0.209,-0.572 h -0.08 v 0.228 h 0.082 c 0.039,0 0.07,-0.009 0.094,-0.027 0.024,-0.017 0.037,-0.045 0.04,-0.083 0,-0.044 -0.012,-0.075 -0.036,-0.092 -0.024,-0.017 -0.057,-0.026 -0.1,-0.026 z"
|
||||
style="fill:#ffffff;fill-rule:nonzero"
|
||||
id="path202" />
|
||||
</g>
|
||||
<g
|
||||
transform="matrix(64,0,0,64,79.7117,77.6203)"
|
||||
id="g208">
|
||||
<path
|
||||
d="M 0.332,0 H 0.084 v -0.68 h 0.252 c 0.105,0 0.182,0.032 0.233,0.095 0.051,0.063 0.076,0.144 0.076,0.241 0,0.105 -0.027,0.189 -0.082,0.251 C 0.508,-0.031 0.431,0 0.332,0 Z M 0.337,-0.57 H 0.213 v 0.461 H 0.33 c 0.055,0 0.099,-0.018 0.132,-0.054 C 0.495,-0.199 0.511,-0.259 0.511,-0.344 0.511,-0.415 0.497,-0.47 0.469,-0.51 0.441,-0.55 0.397,-0.57 0.337,-0.57 Z"
|
||||
style="fill:#ffffff;fill-rule:nonzero"
|
||||
id="path206" />
|
||||
</g>
|
||||
<g
|
||||
transform="matrix(64,0,0,64,123.424,77.6203)"
|
||||
id="g212">
|
||||
<path
|
||||
d="M 0.405,-0.288 H 0.213 V 0 H 0.084 v -0.68 h 0.385 l 0.02,0.102 H 0.213 v 0.189 h 0.173 z"
|
||||
style="fill:#ffffff;fill-rule:nonzero"
|
||||
id="path210" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
transform="matrix(1,0,0,1.52217,67.3796,10.7507)"
|
||||
id="g222">
|
||||
<rect
|
||||
x="23.500999"
|
||||
y="81.300003"
|
||||
width="162.30499"
|
||||
height="61.77"
|
||||
style="fill:#b4d5ff"
|
||||
id="rect220" />
|
||||
</g>
|
||||
<g
|
||||
transform="matrix(0.967536,0,0,0.961535,5.90498,47.9703)"
|
||||
id="g236">
|
||||
<g
|
||||
transform="matrix(90.4804,0,0,90.4804,82.6698,167.705)"
|
||||
id="g226">
|
||||
<path
|
||||
d="m 0.057,-0.337 c 0,-0.105 0.027,-0.19 0.082,-0.257 0.055,-0.066 0.132,-0.1 0.231,-0.102 0.107,0 0.186,0.034 0.237,0.103 0.051,0.069 0.077,0.152 0.077,0.249 0,0.105 -0.027,0.191 -0.082,0.258 -0.055,0.067 -0.133,0.1 -0.232,0.1 C 0.264,0.014 0.185,-0.02 0.134,-0.089 0.083,-0.157 0.057,-0.24 0.057,-0.337 Z m 0.135,-0.001 c 0,0.071 0.014,0.13 0.043,0.175 0.029,0.045 0.073,0.068 0.134,0.068 0.055,0 0.098,-0.02 0.131,-0.061 0.033,-0.041 0.049,-0.103 0.049,-0.188 0,-0.071 -0.014,-0.129 -0.043,-0.174 -0.029,-0.045 -0.073,-0.068 -0.134,-0.068 -0.053,0 -0.097,0.022 -0.13,0.067 -0.033,0.045 -0.05,0.105 -0.05,0.181 z"
|
||||
style="fill:#333333;fill-rule:nonzero"
|
||||
id="path224" />
|
||||
</g>
|
||||
<g
|
||||
transform="matrix(90.4804,0,0,90.4804,147.906,167.705)"
|
||||
id="g230">
|
||||
<path
|
||||
d="M 0.505,-0.557 C 0.473,-0.567 0.448,-0.574 0.429,-0.579 0.41,-0.583 0.388,-0.585 0.361,-0.585 c -0.054,0 -0.096,0.022 -0.125,0.066 -0.029,0.044 -0.044,0.104 -0.044,0.181 0,0.066 0.012,0.123 0.037,0.171 0.025,0.048 0.066,0.072 0.124,0.072 0.029,0 0.056,-0.003 0.081,-0.009 0.025,-0.006 0.047,-0.013 0.068,-0.022 L 0.551,-0.03 C 0.525,-0.017 0.494,-0.006 0.457,0.002 0.42,0.01 0.388,0.014 0.36,0.014 0.254,0.014 0.177,-0.02 0.129,-0.088 0.081,-0.156 0.057,-0.239 0.057,-0.337 c 0,-0.105 0.027,-0.19 0.08,-0.257 0.053,-0.067 0.129,-0.1 0.228,-0.1 0.02,0 0.048,0.003 0.083,0.01 0.035,0.007 0.068,0.018 0.097,0.034 z"
|
||||
style="fill:#333333;fill-rule:nonzero"
|
||||
id="path228" />
|
||||
</g>
|
||||
<g
|
||||
transform="matrix(90.4804,0,0,90.4804,199.751,167.705)"
|
||||
id="g234">
|
||||
<path
|
||||
d="m 0.293,-0.572 h -0.08 v 0.208 h 0.082 c 0.039,0 0.071,-0.008 0.096,-0.024 0.025,-0.015 0.038,-0.041 0.038,-0.077 0,-0.038 -0.012,-0.065 -0.036,-0.082 -0.024,-0.017 -0.057,-0.025 -0.1,-0.025 z M 0.479,0 0.335,-0.26 C 0.328,-0.259 0.32,-0.259 0.312,-0.259 0.304,-0.258 0.296,-0.258 0.288,-0.258 H 0.213 V 0 H 0.084 v -0.68 h 0.213 c 0.074,0 0.137,0.017 0.19,0.051 0.053,0.034 0.079,0.087 0.079,0.158 0,0.042 -0.011,0.078 -0.032,0.108 -0.022,0.031 -0.05,0.054 -0.084,0.071 L 0.617,0 Z"
|
||||
style="fill:#333333;fill-rule:nonzero"
|
||||
id="path232" />
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
transform="matrix(0.916882,0,0,1,121.475,-32.6535)"
|
||||
id="g246">
|
||||
<g
|
||||
transform="matrix(86.953,0,0,86.953,152.996,241.878)"
|
||||
id="g240">
|
||||
<path
|
||||
d="M 0.479,-0.428 C 0.5,-0.451 0.527,-0.47 0.562,-0.484 c 0.034,-0.013 0.065,-0.02 0.092,-0.02 0.066,0 0.113,0.019 0.141,0.058 0.027,0.039 0.041,0.086 0.041,0.142 V 0 H 0.705 v -0.298 c 0,-0.031 -0.007,-0.054 -0.022,-0.071 -0.015,-0.016 -0.036,-0.024 -0.064,-0.024 -0.019,0 -0.038,0.005 -0.059,0.015 -0.021,0.01 -0.039,0.021 -0.056,0.034 0.001,0.007 0.001,0.013 0.002,0.02 0.001,0.007 0.001,0.013 0.001,0.02 V 0 H 0.376 v -0.298 c 0,-0.031 -0.007,-0.054 -0.022,-0.071 -0.015,-0.016 -0.036,-0.024 -0.063,-0.024 -0.017,0 -0.033,0.003 -0.05,0.01 -0.017,0.007 -0.034,0.016 -0.049,0.027 V 0 H 0.062 V -0.485 H 0.13 l 0.032,0.044 c 0.022,-0.02 0.049,-0.035 0.08,-0.047 0.031,-0.011 0.058,-0.016 0.083,-0.016 0.038,0 0.07,0.007 0.095,0.02 0.025,0.014 0.045,0.033 0.059,0.056 z"
|
||||
style="fill:#333333;fill-rule:nonzero"
|
||||
id="path238" />
|
||||
</g>
|
||||
<g
|
||||
transform="matrix(86.953,0,0,86.953,228.906,241.878)"
|
||||
id="g244">
|
||||
<path
|
||||
d="M 0.156,0.023 0.179,-0.034 0.006,-0.467 0.14,-0.485 0.252,-0.191 0.358,-0.485 H 0.495 L 0.278,0.064 C 0.263,0.103 0.236,0.137 0.197,0.165 0.158,0.193 0.118,0.212 0.075,0.222 L 0.029,0.115 C 0.052,0.105 0.077,0.093 0.104,0.079 0.13,0.064 0.147,0.046 0.156,0.023 Z"
|
||||
style="fill:#333333;fill-rule:nonzero"
|
||||
id="path242" />
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
id="Selectors"
|
||||
transform="matrix(0.965977,0,0,0.807602,67.3796,67.3718)">
|
||||
<g
|
||||
id="Right-selector"
|
||||
serif:id="Right selector">
|
||||
<g
|
||||
transform="matrix(1.03522,0,0,1.23823,2.07044,0)"
|
||||
id="g250">
|
||||
<path
|
||||
d="M 185.806,161.156 V 67.132"
|
||||
style="fill:none;stroke:#4c9fff;stroke-width:4px;stroke-linecap:butt"
|
||||
id="path248" />
|
||||
</g>
|
||||
<g
|
||||
transform="matrix(1.03522,0,0,1.23823,161.788,169.469)"
|
||||
id="g254">
|
||||
<circle
|
||||
cx="31.523001"
|
||||
cy="34.313999"
|
||||
r="10.021"
|
||||
style="fill:#4c9fff;stroke:#4c9fff;stroke-width:4px;stroke-linecap:butt"
|
||||
id="circle252" />
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
id="Left-selector"
|
||||
serif:id="Left selector">
|
||||
<g
|
||||
transform="matrix(1.03522,0,0,1.23823,-170.092,0)"
|
||||
id="g259">
|
||||
<path
|
||||
d="M 185.806,161.156 V 67.132"
|
||||
style="fill:none;stroke:#4c9fff;stroke-width:4px;stroke-linecap:butt"
|
||||
id="path257" />
|
||||
</g>
|
||||
<g
|
||||
transform="matrix(1.03522,0,0,1.23823,-10.3742,28.2274)"
|
||||
id="g263">
|
||||
<circle
|
||||
cx="31.523001"
|
||||
cy="34.313999"
|
||||
r="10.021"
|
||||
style="fill:#4c9fff;stroke:#4c9fff;stroke-width:4px;stroke-linecap:butt"
|
||||
id="circle261" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,75 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 503 227" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
|
||||
<g id="svg" transform="matrix(0.965977,0,0,0.807602,0,0)">
|
||||
<rect x="0" y="0" width="520" height="280" style="fill:white;"/>
|
||||
<g transform="matrix(1.03522,0,0,1.23823,-69.7528,-83.422)">
|
||||
<g transform="matrix(1,0,0,1,243.977,20.0703)">
|
||||
<g id="Page">
|
||||
<g transform="matrix(0.961773,0,0,1.05962,6.19811,-3.01071)">
|
||||
<path d="M328.5,97.682C328.5,96.465 327.983,95.296 327.056,94.418C320.026,87.758 289.442,58.78 282.228,51.944C281.251,51.019 279.901,50.496 278.49,50.496C264.493,50.496 188.083,50.496 167.339,50.496C164.468,50.496 162.141,52.609 162.141,55.214C162.141,83.051 162.141,225.565 162.141,253.4C162.141,256.005 164.468,258.117 167.338,258.117C192.242,258.117 299.159,258.117 323.538,258.117C326.278,258.117 328.5,256.101 328.5,253.613C328.5,229.268 328.5,113.896 328.5,97.682Z" style="fill:rgb(253,253,253);stroke:rgb(51,51,51);stroke-width:3.95px;"/>
|
||||
</g>
|
||||
<g id="Dog-ear" serif:id="Dog ear" transform="matrix(1,0,0,1,-4,2)">
|
||||
<path d="M277.072,48.496L277.072,93.848C277.072,95.172 277.598,96.441 278.534,97.377C279.47,98.313 280.739,98.839 282.063,98.839C294.548,98.839 326.141,98.839 326.141,98.839" style="fill:rgb(245,245,245);stroke:rgb(51,51,51);stroke-width:4px;"/>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-29.6816,-0.395178)">
|
||||
<g transform="matrix(1.00243,0,0,1.11818,-144.72,-8.80181)">
|
||||
<path d="M465.73,119.654C465.73,117.605 463.874,115.941 461.588,115.941L310.259,115.941C307.973,115.941 306.117,117.605 306.117,119.654L306.117,183.108C306.117,185.157 307.973,186.821 310.259,186.821L461.588,186.821C463.874,186.821 465.73,185.157 465.73,183.108L465.73,119.654Z" style="fill:rgb(248,0,0);stroke:white;stroke-width:3.77px;"/>
|
||||
</g>
|
||||
<g transform="matrix(1.24571,0,0,1.35864,116.812,84.3924)">
|
||||
<g transform="matrix(64,0,0,64,42.1437,77.6203)">
|
||||
<path d="M0.084,0L0.084,-0.68L0.297,-0.68C0.371,-0.68 0.434,-0.663 0.487,-0.63C0.54,-0.596 0.566,-0.54 0.566,-0.462C0.566,-0.385 0.538,-0.328 0.481,-0.292C0.424,-0.255 0.36,-0.237 0.288,-0.237L0.213,-0.237L0.213,0L0.084,0ZM0.293,-0.572L0.213,-0.572L0.213,-0.344L0.295,-0.344C0.334,-0.344 0.365,-0.353 0.389,-0.371C0.413,-0.388 0.426,-0.416 0.429,-0.454C0.429,-0.498 0.417,-0.529 0.393,-0.546C0.369,-0.563 0.336,-0.572 0.293,-0.572Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(64,0,0,64,79.7117,77.6203)">
|
||||
<path d="M0.332,0L0.084,0L0.084,-0.68L0.336,-0.68C0.441,-0.68 0.518,-0.648 0.569,-0.585C0.62,-0.522 0.645,-0.441 0.645,-0.344C0.645,-0.239 0.618,-0.155 0.563,-0.093C0.508,-0.031 0.431,0 0.332,0ZM0.337,-0.57L0.213,-0.57L0.213,-0.109L0.33,-0.109C0.385,-0.109 0.429,-0.127 0.462,-0.163C0.495,-0.199 0.511,-0.259 0.511,-0.344C0.511,-0.415 0.497,-0.47 0.469,-0.51C0.441,-0.55 0.397,-0.57 0.337,-0.57Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(64,0,0,64,123.424,77.6203)">
|
||||
<path d="M0.405,-0.288L0.213,-0.288L0.213,0L0.084,0L0.084,-0.68L0.469,-0.68L0.489,-0.578L0.213,-0.578L0.213,-0.389L0.386,-0.389L0.405,-0.288Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1.52217,67.3796,10.7507)">
|
||||
<rect x="23.501" y="81.3" width="162.305" height="61.77" style="fill:rgb(180,213,255);"/>
|
||||
</g>
|
||||
<g transform="matrix(0.967536,0,0,0.961535,5.90498,47.9703)">
|
||||
<g transform="matrix(90.4804,0,0,90.4804,82.6698,167.705)">
|
||||
<path d="M0.057,-0.337C0.057,-0.442 0.084,-0.527 0.139,-0.594C0.194,-0.66 0.271,-0.694 0.37,-0.696C0.477,-0.696 0.556,-0.662 0.607,-0.593C0.658,-0.524 0.684,-0.441 0.684,-0.344C0.684,-0.239 0.657,-0.153 0.602,-0.086C0.547,-0.019 0.469,0.014 0.37,0.014C0.264,0.014 0.185,-0.02 0.134,-0.089C0.083,-0.157 0.057,-0.24 0.057,-0.337ZM0.192,-0.338C0.192,-0.267 0.206,-0.208 0.235,-0.163C0.264,-0.118 0.308,-0.095 0.369,-0.095C0.424,-0.095 0.467,-0.115 0.5,-0.156C0.533,-0.197 0.549,-0.259 0.549,-0.344C0.549,-0.415 0.535,-0.473 0.506,-0.518C0.477,-0.563 0.433,-0.586 0.372,-0.586C0.319,-0.586 0.275,-0.564 0.242,-0.519C0.209,-0.474 0.192,-0.414 0.192,-0.338Z" style="fill:rgb(51,51,51);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(90.4804,0,0,90.4804,147.906,167.705)">
|
||||
<path d="M0.505,-0.557C0.473,-0.567 0.448,-0.574 0.429,-0.579C0.41,-0.583 0.388,-0.585 0.361,-0.585C0.307,-0.585 0.265,-0.563 0.236,-0.519C0.207,-0.475 0.192,-0.415 0.192,-0.338C0.192,-0.272 0.204,-0.215 0.229,-0.167C0.254,-0.119 0.295,-0.095 0.353,-0.095C0.382,-0.095 0.409,-0.098 0.434,-0.104C0.459,-0.11 0.481,-0.117 0.502,-0.126L0.551,-0.03C0.525,-0.017 0.494,-0.006 0.457,0.002C0.42,0.01 0.388,0.014 0.36,0.014C0.254,0.014 0.177,-0.02 0.129,-0.088C0.081,-0.156 0.057,-0.239 0.057,-0.337C0.057,-0.442 0.084,-0.527 0.137,-0.594C0.19,-0.661 0.266,-0.694 0.365,-0.694C0.385,-0.694 0.413,-0.691 0.448,-0.684C0.483,-0.677 0.516,-0.666 0.545,-0.65L0.505,-0.557Z" style="fill:rgb(51,51,51);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(90.4804,0,0,90.4804,199.751,167.705)">
|
||||
<path d="M0.293,-0.572L0.213,-0.572L0.213,-0.364L0.295,-0.364C0.334,-0.364 0.366,-0.372 0.391,-0.388C0.416,-0.403 0.429,-0.429 0.429,-0.465C0.429,-0.503 0.417,-0.53 0.393,-0.547C0.369,-0.564 0.336,-0.572 0.293,-0.572ZM0.479,0L0.335,-0.26C0.328,-0.259 0.32,-0.259 0.312,-0.259C0.304,-0.258 0.296,-0.258 0.288,-0.258L0.213,-0.258L0.213,0L0.084,0L0.084,-0.68L0.297,-0.68C0.371,-0.68 0.434,-0.663 0.487,-0.629C0.54,-0.595 0.566,-0.542 0.566,-0.471C0.566,-0.429 0.555,-0.393 0.534,-0.363C0.512,-0.332 0.484,-0.309 0.45,-0.292L0.617,0L0.479,0Z" style="fill:rgb(51,51,51);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="matrix(0.916882,0,0,1,121.475,-32.6535)">
|
||||
<g transform="matrix(86.953,0,0,86.953,152.996,241.878)">
|
||||
<path d="M0.479,-0.428C0.5,-0.451 0.527,-0.47 0.562,-0.484C0.596,-0.497 0.627,-0.504 0.654,-0.504C0.72,-0.504 0.767,-0.485 0.795,-0.446C0.822,-0.407 0.836,-0.36 0.836,-0.304L0.836,0L0.705,0L0.705,-0.298C0.705,-0.329 0.698,-0.352 0.683,-0.369C0.668,-0.385 0.647,-0.393 0.619,-0.393C0.6,-0.393 0.581,-0.388 0.56,-0.378C0.539,-0.368 0.521,-0.357 0.504,-0.344C0.505,-0.337 0.505,-0.331 0.506,-0.324C0.507,-0.317 0.507,-0.311 0.507,-0.304L0.507,0L0.376,0L0.376,-0.298C0.376,-0.329 0.369,-0.352 0.354,-0.369C0.339,-0.385 0.318,-0.393 0.291,-0.393C0.274,-0.393 0.258,-0.39 0.241,-0.383C0.224,-0.376 0.207,-0.367 0.192,-0.356L0.192,0L0.062,0L0.062,-0.485L0.13,-0.485L0.162,-0.441C0.184,-0.461 0.211,-0.476 0.242,-0.488C0.273,-0.499 0.3,-0.504 0.325,-0.504C0.363,-0.504 0.395,-0.497 0.42,-0.484C0.445,-0.47 0.465,-0.451 0.479,-0.428Z" style="fill:rgb(51,51,51);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(86.953,0,0,86.953,228.906,241.878)">
|
||||
<path d="M0.156,0.023L0.179,-0.034L0.006,-0.467L0.14,-0.485L0.252,-0.191L0.358,-0.485L0.495,-0.485L0.278,0.064C0.263,0.103 0.236,0.137 0.197,0.165C0.158,0.193 0.118,0.212 0.075,0.222L0.029,0.115C0.052,0.105 0.077,0.093 0.104,0.079C0.13,0.064 0.147,0.046 0.156,0.023Z" style="fill:rgb(51,51,51);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="Selectors" transform="matrix(0.965977,0,0,0.807602,67.3796,67.3718)">
|
||||
<g id="Right-selector" serif:id="Right selector">
|
||||
<g transform="matrix(1.03522,0,0,1.23823,2.07044,0)">
|
||||
<path d="M185.806,161.156L185.806,67.132" style="fill:none;stroke:rgb(76,159,255);stroke-width:4px;stroke-linecap:butt;"/>
|
||||
</g>
|
||||
<g transform="matrix(1.03522,0,0,1.23823,161.788,169.469)">
|
||||
<circle cx="31.523" cy="34.314" r="10.021" style="fill:rgb(76,159,255);stroke:rgb(76,159,255);stroke-width:4px;stroke-linecap:butt;"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="Left-selector" serif:id="Left selector">
|
||||
<g transform="matrix(1.03522,0,0,1.23823,-170.092,0)">
|
||||
<path d="M185.806,161.156L185.806,67.132" style="fill:none;stroke:rgb(76,159,255);stroke-width:4px;stroke-linecap:butt;"/>
|
||||
</g>
|
||||
<g transform="matrix(1.03522,0,0,1.23823,-10.3742,28.2274)">
|
||||
<circle cx="31.523" cy="34.314" r="10.021" style="fill:rgb(76,159,255);stroke:rgb(76,159,255);stroke-width:4px;stroke-linecap:butt;"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 21 KiB |
@@ -1,28 +1,45 @@
|
||||
.. ocrmypdf documentation master file, created by
|
||||
sphinx-quickstart on Sun Sep 4 14:29:43 2016.
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
OCRmyPDF documentation
|
||||
======================
|
||||
|
||||
OCRmyPDF adds an OCR text layer to scanned PDF files, allowing them to
|
||||
be searched.
|
||||
.. figure:: images/logo.svg
|
||||
|
||||
PDFs are the best format for scanned documents. Unfortunately, PDFs can be difficult to work with. OCRmyPDF makes it easy to apply image processing and OCR to existing PDFs.
|
||||
OCRmyPDF adds an optical character recognition (OCR) text layer to scanned PDF
|
||||
files, allowing them to be searched.
|
||||
|
||||
Contents:
|
||||
PDF is the best format for storing and exchanging scanned documents.
|
||||
Unfortunately, PDFs can be difficult to modify. OCRmyPDF makes it easy to apply
|
||||
image processing and OCR to existing PDFs.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:maxdepth: 1
|
||||
|
||||
introduction
|
||||
release_notes
|
||||
installation
|
||||
optimizer
|
||||
languages
|
||||
jbig2
|
||||
|
||||
.. toctree::
|
||||
:caption: Usage
|
||||
:maxdepth: 2
|
||||
|
||||
cookbook
|
||||
docker
|
||||
advanced
|
||||
batch
|
||||
performance
|
||||
pdfsecurity
|
||||
errors
|
||||
|
||||
.. toctree::
|
||||
:caption: Developers
|
||||
:maxdepth: 2
|
||||
|
||||
api
|
||||
plugins
|
||||
apiref
|
||||
contributing
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
@@ -30,4 +47,3 @@ Indices and tables
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
|
||||
|
||||
@@ -1,71 +1,239 @@
|
||||
============
|
||||
Introduction
|
||||
============
|
||||
|
||||
OCRmyPDF is a Python 3 package that adds OCR layers to PDFs.
|
||||
|
||||
OCRmyPDF is an application and library that adds text "layers" to images
|
||||
in PDFs, making scanned image PDFs searchable. It uses OCR to guess what text
|
||||
is contained in images. It is written in Python. OCRmyPDF supports plugins
|
||||
that allow customization of its processing steps, and is very tolerant of
|
||||
PDFs that contain scanned images and "born digital" content that needs no
|
||||
text recognition.
|
||||
|
||||
About OCR
|
||||
---------
|
||||
=========
|
||||
|
||||
`Optical character recognition <https://en.wikipedia.org/wiki/Optical_character_recognition>`_ is technology that converts images of typed or handwritten text, such as in a scanned document, to computer text that can be searched and copied.
|
||||
`Optical character
|
||||
recognition <https://en.wikipedia.org/wiki/Optical_character_recognition>`__
|
||||
is technology that converts images of typed or handwritten text, such as
|
||||
in a scanned document, to computer text that can be selected, searched and copied.
|
||||
|
||||
OCRmyPDF uses `Tesseract <https://github.com/tesseract-ocr/tesseract>`_, the best available open source OCR engine, to perform OCR.
|
||||
OCRmyPDF uses
|
||||
`Tesseract <https://github.com/tesseract-ocr/tesseract>`__, the best
|
||||
available open source OCR engine, to perform OCR.
|
||||
|
||||
.. _raster-vector:
|
||||
|
||||
About PDFs
|
||||
----------
|
||||
==========
|
||||
|
||||
PDFs are page description files that attempts to preserve a layout exactly. They can contain `vector graphic files <http://vector-conversions.com/vectorizing/raster_vs_vector.html>`_ that can contain raster objects such as scanned images. Because PDFs can contain multiple pages (unlike many image formats) and can contain fonts and text, it is a good formats for exchanging scanned documents.
|
||||
PDFs are page description files that attempts to preserve a layout
|
||||
exactly. They contain `vector
|
||||
graphics <http://vector-conversions.com/vectorizing/raster_vs_vector.html>`__
|
||||
that can contain raster objects such as scanned images. Because PDFs can
|
||||
contain multiple pages (unlike many image formats) and can contain fonts
|
||||
and text, it is a good format for exchanging scanned documents.
|
||||
|
||||
.. image:: bitmap_vs_svg.svg
|
||||
|image|
|
||||
|
||||
A PDF page might contain multiple images, even if it only appears to have one image. Some scanners or scanning software will segment pages into monochromatic text and color regions for example, to improve the compression ratio and appearance of the page.
|
||||
|
||||
Rasterizing a PDF is the process of generating an image suitable for display or analyzing with an OCR engine. OCR engines like Tesseract work with images, not vector objects.
|
||||
A PDF page might contain multiple images, even if it only appears to
|
||||
have one image. Some scanners or scanning software will segment pages
|
||||
into monochromatic text and color regions for example, to improve the
|
||||
compression ratio and appearance of the page.
|
||||
|
||||
Rasterizing a PDF is the process of generating corresponding raster images.
|
||||
OCR engines like Tesseract work with images, not scalable vector graphics
|
||||
or mixed raster-vector-text graphics such as PDF.
|
||||
|
||||
About PDF/A
|
||||
-----------
|
||||
===========
|
||||
|
||||
`PDF/A <https://en.wikipedia.org/wiki/PDF/A>`_ is a standardized subset of the full PDF specification that is designed for archiving. PDF/A differs from PDF primarily by omitting features that would make it difficult to read the file in the future, such as embedded Javascript or references to external fonts. All fonts and resources needed to interpret the PDF must be contained within it. Generally speaking, scanned documents should be converted to PDF/A. There are various conformance levels and versions, such as "PDF/A-2b".
|
||||
`PDF/A <https://en.wikipedia.org/wiki/PDF/A>`__ is an ISO-standardized
|
||||
subset of the full PDF specification that is designed for archiving (the
|
||||
'A' stands for Archive). PDF/A differs from PDF primarily by omitting
|
||||
features that would make it difficult to read the file in the future,
|
||||
such as embedded Javascript, video, audio and references to external
|
||||
fonts. All fonts and resources needed to interpret the PDF must be
|
||||
contained within it. Because PDF/A disables Javascript and other types
|
||||
of embedded content, it is probably more secure.
|
||||
|
||||
Since most people who scan documents are interested in reading them in the future, OCRmyPDF generates PDF/A-2b by default.
|
||||
There are various conformance levels and versions, such as "PDF/A-2b".
|
||||
|
||||
PDF/A has a few drawbacks. Some PDF viewers include an alert that the file is a PDF/A, which may confuse some users. It also tends to produce larger files than PDF, because it embeds certain resources even if they are commonly available.
|
||||
Generally speaking, the best format for scanned documents is PDF/A. Some
|
||||
governments and jurisdictions, US Courts in particular, `mandate the use
|
||||
of PDF/A <https://pdfblog.com/2012/02/13/what-is-pdfa/>`__ for scanned
|
||||
documents.
|
||||
|
||||
Since most people who scan documents are interested in reading them
|
||||
indefinitely into the future, OCRmyPDF generates PDF/A-2b by default.
|
||||
|
||||
PDF/A has a few drawbacks. Some PDF viewers include an alert that the
|
||||
file is a PDF/A, which may confuse some users. It also tends to produce
|
||||
larger files than PDF, because it embeds certain resources even if they
|
||||
are commonly available. PDF/A files can be digitally signed, but may not
|
||||
be encrypted, to ensure they can be read in the future. Fortunately,
|
||||
converting from PDF/A to a regular PDF is trivial, and any PDF viewer
|
||||
can view PDF/A.
|
||||
|
||||
What OCRmyPDF does
|
||||
------------------
|
||||
==================
|
||||
|
||||
OCRmyPDF analyzes each page of a PDF to determine the colorspace and resolution (DPI) needed to capture all of the information on that page without losing content. It uses `Ghostscript <http://ghostscript.com/>`_ to rasterize the page, and then performs on OCR on the rasterized image. It is not enough to simply extract the images from each page and run OCR on them individually. Of course one could use Ghostscript or another PDF rasterizer and then pass the image to Tesseract. OCRmyPDF automates this process and produces a minimally changed output file that contains the same information, colorspace and resolution.
|
||||
OCRmyPDF analyzes each page of a PDF to determine the colorspace and
|
||||
resolution (DPI) needed to capture all of the information on that page
|
||||
without losing content. It uses
|
||||
`Ghostscript <http://ghostscript.com/>`__ to rasterize the page, and
|
||||
then performs on OCR the rasterized image to create an OCR "layer".
|
||||
The layer is then grafted back onto the original PDF.
|
||||
|
||||
The Tesseract OCR engine can output 'hOCR' files, which are XML files that contain a description of the text it found on the page. OCRmyPDF will render a new PDF that contains only the hidden text layer, and merge this with the original page.
|
||||
While one can use a program like Ghostscript or ImageMagick to get an
|
||||
image and put the image through Tesseract, that actually creates a new
|
||||
PDF and many details may be lost. OCRmyPDF can produce a minimally
|
||||
changed PDF as output.
|
||||
|
||||
Alternately, OCRmyPDF can use the Tesseract OCR engine to directly output PDFs for each page, then merge them.
|
||||
OCRmyPDF also provides some image processing options, like deskew, which
|
||||
improves the appearance of files and quality of OCR. When these are used,
|
||||
the OCR layer is grafted onto the processed image instead.
|
||||
|
||||
By default, OCRmyPDF will convert the file to a PDF/A. This behavior can be disabled with the ``--output-type pdf`` argument.
|
||||
By default, OCRmyPDF produces archival PDFs – PDF/A, which are a
|
||||
stricter subset of PDF features designed for long term archives. If
|
||||
regular PDFs are desired, this can be disabled with
|
||||
``--output-type pdf``.
|
||||
|
||||
Depending on the settings selected, OCRmyPDF may "graft" the OCR layer into the existing PDF, or reconstruct a visually equivalent new PDF.
|
||||
Why you shouldn't do this manually
|
||||
==================================
|
||||
|
||||
A PDF is similar to an HTML file, in that it contains document structure
|
||||
along with images. Sometimes a PDF does nothing more than present a full
|
||||
page image, but often there is additional content that would be lost.
|
||||
|
||||
A manual process could work like either of these:
|
||||
|
||||
1. Rasterize each page as an image, OCR the images, and combine the
|
||||
output into a PDF. This preserves the layout of each page, but
|
||||
resamples all images (possibly losing quality, increasing file size,
|
||||
introducing compression artifacts, etc.).
|
||||
2. Extract each image, OCR, and combine the output into a PDF. This
|
||||
loses the context in which images are used in the PDF, meaning that
|
||||
cropping, rotation and scaling of pages may be lost. Some scanned
|
||||
PDFs use multiple images segmented into black and white, grayscale
|
||||
and color regions, with stencil masks to prevent overlap, as this can
|
||||
enhance the appearance of a file while reducing file size. Clearly,
|
||||
reassembling these images will be easy. This also loses and text or
|
||||
vector art on any pages in a PDF with both scanned and pure digital
|
||||
content.
|
||||
|
||||
In the case of a PDF that is nothing other than a container of images
|
||||
(no rotation, scaling, cropping, one image per page), the second
|
||||
approach can be lossless.
|
||||
|
||||
OCRmyPDF uses several strategies depending on input options and the
|
||||
input PDF itself, but generally speaking it rasterizes a page for OCR
|
||||
and then grafts the OCR back onto the original. As such it can handle
|
||||
complex PDFs and still preserve their contents as much as possible.
|
||||
|
||||
OCRmyPDF also supports a many, many edge cases that have cropped over
|
||||
several years of development. We support PDF features like images inside
|
||||
of Form XObjects, and pages with UserUnit scaling. We support rare image
|
||||
formats like non-monochrome 1-bit images. We warn about files you may
|
||||
not to OCR. Thanks to pikepdf and QPDF, we auto-repair PDFs that are
|
||||
damaged. (Not that you need to know what any of these are! You should be
|
||||
able to throw any PDF at it.)
|
||||
|
||||
Limitations
|
||||
-----------
|
||||
===========
|
||||
|
||||
OCRmyPDF is limited by the Tesseract OCR engine. As such it experiences these limitations, as do any other programs that rely on Tesseract:
|
||||
OCRmyPDF is limited by the Tesseract OCR engine. As such it experiences
|
||||
these limitations, as do any other programs that rely on Tesseract:
|
||||
|
||||
- The OCR is not as accurate as commercial OCR solutions.
|
||||
- It is not capable of recognizing handwriting.
|
||||
- It may find gibberish and report this as OCR output.
|
||||
- If a document contains languages outside of those given in the
|
||||
``-l LANG`` arguments, results may be poor.
|
||||
- It is not always good at analyzing the natural reading order of
|
||||
documents. For example, it may fail to recognize that a document
|
||||
contains two columns, and may try to join text across columns.
|
||||
- Poor quality scans may produce poor quality OCR. Garbage in, garbage
|
||||
out.
|
||||
- It does not expose information about what font family text belongs
|
||||
to.
|
||||
|
||||
* The OCR is not as accurate as commercial solutions such as Abbyy.
|
||||
* It is not capable of recognizing handwriting.
|
||||
* It may find gibberish and report this as OCR output.
|
||||
* If a document contains languages outside of those given in the ``-l LANG`` arguments, results may be poor.
|
||||
* It is not always good at analyzing the natural reading order of documents. For example, it may fail to recognize that a document contains two columns and join text across the columns.
|
||||
* Poor quality scans may produce poor quality OCR. Garbage in, garbage out.
|
||||
|
||||
OCRmyPDF is also limited by the PDF specification:
|
||||
|
||||
* PDF encodes the position of text glyphs but does not encode document structure. There is no markup that divides a document in sections, paragraphs, sentences, or even words (since blank spaces are not represented). As such all elements of document structure including the spaces between words must be derived heuristically. Some PDF viewers do a better job of this than others.
|
||||
- PDF encodes the position of text glyphs but does not encode document
|
||||
structure. There is no markup that divides a document in sections,
|
||||
paragraphs, sentences, or even words (since blank spaces are not
|
||||
represented). As such all elements of document structure including
|
||||
the spaces between words must be derived heuristically. Some PDF
|
||||
viewers do a better job of this than others.
|
||||
- Because some popular open source PDF viewers have a particularly hard
|
||||
time with spaces between words, OCRmyPDF appends a space to each text
|
||||
element as a workaround (when using ``--pdf-renderer hocr``). While
|
||||
this mixes document structure with graphical information that ideally
|
||||
should be left to the PDF viewer to interpret, it improves
|
||||
compatibility with some viewers and does not cause problems for
|
||||
better ones.
|
||||
|
||||
Ghostscript also imposes some limitations:
|
||||
|
||||
* PDFs containing JBIG2-encoded content will be converted to CCITT Group4 encoding, which has lower compression ratios, if Ghostscript PDF/A is enabled.
|
||||
|
||||
OCRmyPDF is currently not designed to be used as a Python API; it is designed to be run as a command line tool. ``import ocrmypf`` currently attempts to process the command line on ``sys.argv`` at import time so it has side effects that will interfere with its use as a package. The API it presents should not be considered stable.
|
||||
- PDFs containing JBIG2-encoded content will be converted to CCITT
|
||||
Group4 encoding, which has lower compression ratios, if Ghostscript
|
||||
PDF/A is enabled.
|
||||
- PDFs containing JPEG 2000-encoded content will be converted to JPEG
|
||||
encoding, which may introduce compression artifacts, if Ghostscript
|
||||
PDF/A is enabled.
|
||||
- Ghostscript may transcode grayscale and color images, either lossy to
|
||||
lossless or lossless to lossy, based on an internal algorithm. This
|
||||
behavior can be suppressed by setting ``--pdfa-image-compression`` to
|
||||
``jpeg`` or ``lossless`` to set all images to one type or the other.
|
||||
Ghostscript has no option to maintain the input image's format.
|
||||
(Ghostscript 9.25+ can copy JPEG images without transcoding them;
|
||||
earlier versions will transcode.)
|
||||
- Ghostscript's PDF/A conversion removes any XMP metadata that is not
|
||||
one of the standard XMP metadata namespaces for PDFs. In particular,
|
||||
PRISM Metdata is removed.
|
||||
- Ghostscript's PDF/A conversion seems to remove or deactivate
|
||||
hyperlinks and other active content.
|
||||
|
||||
You can use ``--output-type pdf`` to disable PDF/A conversion and produce
|
||||
a standard, non-archival PDF.
|
||||
|
||||
Regarding OCRmyPDF itself:
|
||||
|
||||
- PDFs that use transparency are not currently represented in the test
|
||||
suite
|
||||
|
||||
Similar programs
|
||||
================
|
||||
|
||||
To the author's knowledge, OCRmyPDF is the most feature-rich and
|
||||
thoroughly tested command line OCR PDF conversion tool. If it does not
|
||||
meet your needs, contributions and suggestions are welcome. If not,
|
||||
consider one of these similar open source programs:
|
||||
|
||||
- pdf2pdfocr
|
||||
- pdfsandwich
|
||||
|
||||
Ghostscript recently added three "pdfocr" output devices. They work by
|
||||
rasterizing all content and converting all pages to a single colour space.
|
||||
|
||||
Web front-ends
|
||||
==============
|
||||
|
||||
The Docker image ``ocrmypdf`` provides a web service front-end
|
||||
that allows files to submitted over HTTP and the results "downloaded".
|
||||
This is an HTTP server intended to simplify web services deployments; it
|
||||
is not intended to be deployed on the public internet and no real
|
||||
security measures to speak of.
|
||||
|
||||
In addition, the following third-party integrations are available:
|
||||
|
||||
- `Nextcloud OCR <https://github.com/janis91/ocr>`__ is a free software
|
||||
plugin for the Nextcloud private cloud software
|
||||
|
||||
OCRmyPDF is not designed to be secure against malware-bearing PDFs (see
|
||||
`Using OCRmyPDF online <ocr-service>`__). Users should ensure they
|
||||
comply with OCRmyPDF's licenses and the licenses of all dependencies. In
|
||||
particular, OCRmyPDF requires Ghostscript, which is licensed under
|
||||
AGPLv3.
|
||||
|
||||
.. |image| image:: images/bitmap_vs_svg.svg
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
.. _jbig2:
|
||||
|
||||
============================
|
||||
Installing the JBIG2 encoder
|
||||
============================
|
||||
|
||||
Most Linux distributions do not include a JBIG2 encoder since JBIG2
|
||||
encoding was patented for a long time. All known JBIG2 US patents have
|
||||
expired as of 2017, but it is possible that unknown patents exist.
|
||||
|
||||
JBIG2 encoding is recommended for OCRmyPDF and is used to losslessly
|
||||
create smaller PDFs. If JBIG2 encoding is not available, lower quality
|
||||
encodings will be used.
|
||||
|
||||
JBIG2 decoding is not patented and is performed automatically by most
|
||||
PDF viewers. It is widely supported and has been part of the PDF
|
||||
specification since 2001.
|
||||
|
||||
On macOS, Homebrew packages jbig2enc and OCRmyPDF includes it by
|
||||
default. The Docker image for OCRmyPDF also builds its own JBIG2 encoder
|
||||
from source.
|
||||
|
||||
For all other Linux, you must build a JBIG2 encoder from source:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
git clone https://github.com/agl/jbig2enc
|
||||
cd jbig2enc
|
||||
./autogen.sh
|
||||
./configure && make
|
||||
[sudo] make install
|
||||
|
||||
.. _jbig2-lossy:
|
||||
|
||||
Lossy mode JBIG2
|
||||
================
|
||||
|
||||
OCRmyPDF provides lossy mode JBIG2 as an advanced feature. Users should
|
||||
`review the technical concerns with JBIG2 in lossy
|
||||
mode <https://en.wikipedia.org/wiki/JBIG2#Disadvantages>`__
|
||||
and decide if this feature is acceptable for their use case.
|
||||
|
||||
JBIG2 lossy mode does achieve higher compression ratios than any other
|
||||
monochrome (bitonal) compression technology; for large text documents
|
||||
the savings are considerable. JBIG2 lossless still gives great
|
||||
compression ratios and is a major improvement over the older CCITT G4
|
||||
standard. As explained above, there is some risk of substitution errors.
|
||||
|
||||
To turn on JBIG2 lossy mode, add the argument ``--jbig2-lossy``.
|
||||
``--optimize {1,2,3}`` are necessary for the argument to take effect
|
||||
also required. Also, a JBIG2 encoder must be installed as described in
|
||||
the previous section.
|
||||
|
||||
*Due to an oversight, ocrmypdf v7.0 and v7.1 used lossy mode by
|
||||
default.*
|
||||
@@ -1,46 +1,76 @@
|
||||
.. _lang-packs:
|
||||
|
||||
====================================
|
||||
Installing additional language packs
|
||||
====================================
|
||||
|
||||
OCRmyPDF uses Tesseract for OCR, and relies on its language packs for languages other than English.
|
||||
OCRmyPDF uses Tesseract for OCR, and relies on its language packs for all languages.
|
||||
On most platforms, English is installed with Tesseract by default, but not always.
|
||||
|
||||
Tesseract supports `most languages <https://github.com/tesseract-ocr/tesseract/blob/master/doc/tesseract.1.asc#languages>`_.
|
||||
Tesseract supports `most
|
||||
languages <https://github.com/tesseract-ocr/tesseract/blob/master/doc/tesseract.1.asc#languages>`__.
|
||||
Languages are identified by standardized three-letter codes (called ISO 639-2 Alpha-3).
|
||||
Tesseract's documentation also lists the three-letter code for your language.
|
||||
Some are anglicized, e.g. Spanish is ``spa`` rather than ``esp``, while others
|
||||
are not, e.g. German is ``deu`` and French is ``fra``.
|
||||
|
||||
For Linux users, you can often find packages that provide language packs:
|
||||
After you have installed a language pack, you can use it with ``ocrmypdf -l <language>``,
|
||||
for example ``ocrmypdf -l spa``. For multilingual documents, you can specify
|
||||
all languages to be expected, e.g. ``ocrmypdf -l eng+fra`` for English and French.
|
||||
English is assumed by default unless other language(s) are specified.
|
||||
|
||||
For Linux users, you can often find packages that provide language
|
||||
packs:
|
||||
|
||||
Debian and Ubuntu users
|
||||
-----------------------
|
||||
=======================
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# Display a list of all Tesseract language packs
|
||||
apt-cache search tesseract-ocr
|
||||
|
||||
# Debian/Ubuntu users
|
||||
apt-get install tesseract-ocr-chi-sim # Example: Install Chinese Simplified language back
|
||||
|
||||
You can then pass the ``-l LANG`` argument to OCRmyPDF to give a hint as to what languages it should search for. Multiple
|
||||
languages can be requested.
|
||||
# Install Chinese Simplified language pack
|
||||
apt-get install tesseract-ocr-chi-sim
|
||||
|
||||
Mac OS X (macOS) users
|
||||
----------------------
|
||||
You can then pass the ``-l LANG`` argument to OCRmyPDF to give a hint as
|
||||
to what languages it should search for. Multiple languages can be
|
||||
requested using either ``-l eng+fra`` (English and French) or
|
||||
``-l eng -l fra``.
|
||||
|
||||
You can install additional language packs by :ref:`installing Tesseract using Homebrew with all language packs <macos-all-languages>`.
|
||||
|
||||
Docker users
|
||||
------------
|
||||
|
||||
Users of the Docker image may use the alternative :ref:`"polyglot" container <docker-polyglot>` which includes all languages.
|
||||
|
||||
Known limitations
|
||||
-----------------
|
||||
|
||||
As of v4.2, users of ocrmypdf working languages outside the Latin alphabet should use the following syntax:
|
||||
Fedora users
|
||||
============
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf --output-type pdf --pdf-renderer tesseract
|
||||
# Display a list of all Tesseract language packs
|
||||
dnf search tesseract
|
||||
|
||||
The reasons for this are:
|
||||
# Install Chinese Simplified language pack
|
||||
dnf install tesseract-langpack-chi_sim
|
||||
|
||||
* The latest version of Ghostscript (9.19 as of this writing) has unfixed bugs in Unicode handling that generate invalid character maps, so Ghostscript cannot be used for PDF/A conversion
|
||||
* The default "hocr" PDF renderer does not handle Asian fonts properly
|
||||
You can then pass the ``-l LANG`` argument to OCRmyPDF to give a hint as
|
||||
to what languages it should search for. Multiple languages can be
|
||||
requested using either ``-l eng+fra`` (English and French) or
|
||||
``-l eng -l fra``.
|
||||
|
||||
macOS users
|
||||
===========
|
||||
|
||||
You can install additional language packs by
|
||||
:ref:`installing Tesseract using Homebrew with all language packs <macos-all-languages>`.
|
||||
|
||||
Docker users
|
||||
============
|
||||
|
||||
Users of the OCRmyPDF Docker image should install language packs into a
|
||||
derived Docker image as
|
||||
:ref:`described in that section <docker-lang-packs>`.
|
||||
|
||||
Windows users
|
||||
=============
|
||||
|
||||
The Tesseract installer provided by Chocolatey currently includes only English language.
|
||||
To install other languages, download the respective language pack (``.traineddata`` file)
|
||||
from https://github.com/tesseract-ocr/tessdata/ and place it in
|
||||
``C:\\Program Files\\Tesseract-OCR\\tessdata`` (or wherever Tesseract OCR is installed).
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
================
|
||||
PDF optimization
|
||||
================
|
||||
|
||||
OCRmyPDF includes an image-oriented PDF optimizer. By default, the optimizer
|
||||
runs with safe settings with the goal of improving compression at no loss of
|
||||
quality. At higher optimization levels, lossy optimizations may be applied and
|
||||
tuned. Optimization occurs after OCR, and only if OCR succeeded. It does not
|
||||
perform other possible optimizations such as deduplicating resources,
|
||||
consolidating fonts, simplifying vector drawings, or anything of that nature.
|
||||
|
||||
Optimization ranges from ``-O0`` through ``-O3``, where ``0`` disables
|
||||
optimization and ``3`` implements all options. ``1``, the default, performs only
|
||||
safe and lossless optimizations. (This is similar to GCC's optimization
|
||||
parameter.) The exact type of optimizations performed will vary over time.
|
||||
|
||||
PDF optimization requires third-party, optional tools for certain optimizations.
|
||||
If these are not installed or cannot be found by OCRmyPDF, optimization will not
|
||||
be as good.
|
||||
|
||||
Optimizations that always occurs
|
||||
================================
|
||||
|
||||
OCRmyPDF will automatically replace obsolete or inferior compression schemes
|
||||
such as RLE or LZW with superior schemes such as Deflate and converting
|
||||
monochrome images to CCITT G4. Since this is harmless it always occurs and there
|
||||
is no way to disable it. Other non-image compressed objects are compressed as
|
||||
well.
|
||||
|
||||
Fast web view
|
||||
=============
|
||||
|
||||
OCRmyPDF automatically optimizes PDFs for "fast web view" in Adobe Acrobat's
|
||||
parlance, or equivalently, linearizes PDFs so that the resources they reference
|
||||
are presented in the order a viewer needs them for sequential display. This
|
||||
reduces the latency of viewing a PDF both online and from local storage. This
|
||||
actually slightly increases the file size.
|
||||
|
||||
To disable this optimization and all others, use ``ocrmypdf --optimize 0 ...``
|
||||
or the shorthand ``-O0``.
|
||||
|
||||
Lossless optimizations
|
||||
======================
|
||||
|
||||
At optimization level ``-O1`` (the default), OCRmyPDF will also attempt lossless
|
||||
image optimization.
|
||||
|
||||
If a JBIG2 encoder is available, then monochrome images will be converted to
|
||||
JBIG2, with the potential for huge savings on large black and white images,
|
||||
since JBIG2 is far more efficient than any other monochrome (bi-level)
|
||||
compression. (All known US patents related to JBIG2 have probably expired, but
|
||||
it remains the responsibility of the user to supply a JBIG2 encoder such as
|
||||
`jbig2enc <https://github.com/agl/jbig2enc>`__. OCRmyPDF does not implement
|
||||
JBIG2 encoding on its own.)
|
||||
|
||||
OCRmyPDF currently does not attempt to recompress losslessly compressed objects
|
||||
more aggressively.
|
||||
|
||||
Lossy optimizations
|
||||
===================
|
||||
|
||||
At optimization level ``-O2`` and ``-O3``, OCRmyPDF will some attempt lossy
|
||||
image optimization.
|
||||
|
||||
If ``pngquant`` is installed, OCRmyPDF will use it to perform quantize paletted
|
||||
images to reduce their size.
|
||||
|
||||
The quality of JPEGs may be lowered, on the assumption that a lower quality
|
||||
image may be suitable for storage after OCR.
|
||||
|
||||
It is not possible to optimize all image types. Uncommon image types may be
|
||||
skipped by the optimizer.
|
||||
|
||||
OCRmyPDF provides :ref:`lossy mode JBIG2 <jbig2-lossy>` as an advanced feature
|
||||
that additional requires the argument ``--jbig2-lossy``.
|
||||
@@ -0,0 +1,162 @@
|
||||
===================
|
||||
PDF security issues
|
||||
===================
|
||||
|
||||
OCRmyPDF should only be used on PDFs you trust. It is not designed to
|
||||
protect you against malware.
|
||||
|
||||
Recognizing that many users have an interest in handling PDFs and
|
||||
applying OCR to PDFs they did not generate themselves, this article
|
||||
discusses the security implications of PDFs and how users can protect
|
||||
themselves.
|
||||
|
||||
The disclaimer applies: this software has no warranties of any kind.
|
||||
|
||||
PDFs may contain malware
|
||||
========================
|
||||
|
||||
PDF is a rich, complex file format. The official PDF 1.7 specification,
|
||||
ISO 32000:2008, is hundreds of pages long and references several annexes
|
||||
each of which are similar in length. PDFs can contain video, audio, XML,
|
||||
JavaScript and other programming, and forms. In some cases, they can
|
||||
open internet connections to pre-selected URLs. All of these are possible
|
||||
attack vectors.
|
||||
|
||||
In short, PDFs `may contain
|
||||
viruses <https://security.stackexchange.com/questions/64052/can-a-pdf-file-contain-a-virus>`__.
|
||||
|
||||
This
|
||||
`article <https://theinvisiblethings.blogspot.ca/2013/02/converting-untrusted-pdfs-into-trusted.html>`__
|
||||
describes a high-paranoia method which allows potentially hostile PDFs
|
||||
to be viewed and rasterized safely in a disposable virtual machine. A
|
||||
trusted PDF created in this manner is converted to images and loses all
|
||||
information making it searchable and losing all compression. OCRmyPDF
|
||||
could be used to restore searchability.
|
||||
|
||||
How OCRmyPDF processes PDFs
|
||||
===========================
|
||||
|
||||
OCRmyPDF must open and interpret your PDF in order to insert an OCR
|
||||
layer. First, it runs all PDFs through
|
||||
`pikepdf <https://github.com/pikepdf/pikepdf>`__, a library based on
|
||||
`qpdf <https://github.com/qpdf/qpdf>`__, a program that repairs PDFs
|
||||
with syntax errors. This is done because, in the author's experience, a
|
||||
significant number of PDFs in the wild, especially those created by
|
||||
scanners, are not well-formed files. qpdf makes it more likely that
|
||||
OCRmyPDF will succeed, but offers no security guarantees. qpdf is also
|
||||
used to split the PDF into single page PDFs.
|
||||
|
||||
Finally, OCRmyPDF rasterizes each page of the PDF using
|
||||
`Ghostscript <http://ghostscript.com/>`__ in ``-dSAFER`` mode.
|
||||
|
||||
Depending on the options specified, OCRmyPDF may graft the OCR layer
|
||||
into the existing PDF or it may essentially reconstruct ("re-fry") a
|
||||
visually identical PDF that may be quite different at the binary level.
|
||||
That said, OCRmyPDF is not a tool designed for sanitizing PDFs.
|
||||
|
||||
.. _ocr-service:
|
||||
|
||||
Using OCRmyPDF online or as a service
|
||||
=====================================
|
||||
|
||||
OCRmyPDF is not designed for use as a public web service where a
|
||||
malicious user could upload a chosen PDF. In particular, it is not
|
||||
necessarily secure against PDF malware or PDFs that cause denial of
|
||||
service. OCRmyPDF relies on Ghostscript, and therefore, if deployed
|
||||
online one should be prepared to comply with Ghostscript's Affero GPL
|
||||
license, and any other licenses.
|
||||
|
||||
Setting aside these concerns, a side effect of OCRmyPDF is that it may
|
||||
incidentally sanitize PDFs containing certain types of malware. It
|
||||
repairs the PDF with pikepdf/libqpdf, which could correct malformed PDF
|
||||
structures that are part of an attack. When PDF/A output is selected
|
||||
(the default), the input PDF is partially reconstructed by Ghostscript.
|
||||
When ``--force-ocr`` is used, all pages are rasterized and reconverted
|
||||
to PDF, which could remove malware in embedded images.
|
||||
|
||||
OCRmyPDF should be relatively safe to use in a trusted intranet, with
|
||||
some considerations:
|
||||
|
||||
Limiting CPU usage
|
||||
------------------
|
||||
|
||||
OCRmyPDF will attempt to use all available CPUs and storage, so
|
||||
executing ``nice ocrmypdf`` or limiting the number of jobs with the
|
||||
``-j`` argument may ensure the server remains available. Another option
|
||||
would be to run OCRmyPDF jobs inside a Docker container, a virtual machine,
|
||||
or a cloud instance, which can impose its own limits on CPU usage and be
|
||||
terminated "from orbit" if it fails to complete.
|
||||
|
||||
Temporary storage requirements
|
||||
------------------------------
|
||||
|
||||
OCRmyPDF will use a large amount of temporary storage for its work,
|
||||
proportional to the total number of pixels needed to rasterize the PDF.
|
||||
The raster image of a 8.5×11" color page at 300 DPI takes 25 MB
|
||||
uncompressed; OCRmyPDF saves its intermediates as PNG, but that still
|
||||
means it requires about 9 MB per intermediate based on average
|
||||
compression ratios. Multiple intermediates per page are also required,
|
||||
depending on the command line given. A rule of thumb would be to allow
|
||||
100 MB of temporary storage per page in a file – meaning that a small
|
||||
cloud servers or small VM partitions should be provisioned with plenty
|
||||
of extra space, if say, a 500 page file might be sent.
|
||||
|
||||
To check temporary storage usage on actual files, run
|
||||
``ocrmypdf -k ...`` which will preserve and print the path to temporary
|
||||
storage when the job is done.
|
||||
|
||||
To change where temporary files are stored, change the ``TMPDIR``
|
||||
environment variable for ocrmypdf's environment. (Python's
|
||||
``tempfile.gettempdir()`` returns the root directory in which temporary
|
||||
files will be stored.) For example, one could redirect ``TMPDIR`` to a
|
||||
large RAM disk to avoid wear on HDD/SSD and potentially improve
|
||||
performance. On Amazon Web Services, ``TMPDIR`` can be set to `empheral
|
||||
storage <https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/InstanceStorage.html>`__.
|
||||
|
||||
Timeouts
|
||||
--------
|
||||
|
||||
To prevent excessively long OCR jobs consider setting
|
||||
``--tesseract-timeout`` and/or ``--skip-big`` arguments. ``--skip-big``
|
||||
is particularly helpful if your PDFs include documents such as reports
|
||||
on standard page sizes with large images attached - often large images
|
||||
are not worth OCR'ing anyway.
|
||||
|
||||
Commercial alternatives
|
||||
-----------------------
|
||||
|
||||
The author also provides professional services that include OCR and
|
||||
building databases around PDFs, and is happy to provide consultation.
|
||||
|
||||
Abbyy Cloud OCR is viable commercial alternative with a web services
|
||||
API. Amazon Textract, Google Cloud Vision, and Microsoft Azure
|
||||
Computer Vision provide advanced OCR but have less PDF rendering capability.
|
||||
|
||||
Password protection, digital signatures and certification
|
||||
=========================================================
|
||||
|
||||
Password protected PDFs usually have two passwords, and owner and user
|
||||
password. When the user password is set to empty, PDF readers will open
|
||||
the file automatically and marked it as "(SECURED)". While not as
|
||||
reliable as a digital signature, this indicates that whoever set the
|
||||
password approved of the file at that time. When the user password is
|
||||
set, the document cannot be viewed without the password.
|
||||
|
||||
Either way, OCRmyPDF does not remove passwords from PDFs and exits with
|
||||
an error on encountering them.
|
||||
|
||||
``qpdf`` can remove passwords. If the owner and user password are set, a
|
||||
password is required for ``qpdf``. If only the owner password is set, then the
|
||||
password can be stripped, even if one does not have the owner password.
|
||||
|
||||
After OCR is applied, password protection is not permitted on PDF/A
|
||||
documents but the file can be converted to regular PDF.
|
||||
|
||||
Many programs exist which are capable of inserting an image of someone's
|
||||
signature. On its own, this offers no security guarantees. It is trivial
|
||||
to remove the signature image and apply it to other files. This practice
|
||||
offers no real security.
|
||||
|
||||
Important documents can be digitally signed and certified to attest to
|
||||
their authorship. OCRmyPDF cannot do this. Open source tools such as
|
||||
pdfbox (Java) have this capability as does Adobe Acrobat.
|
||||
@@ -0,0 +1,22 @@
|
||||
===========
|
||||
Performance
|
||||
===========
|
||||
|
||||
Some users have noticed that current versions of OCRmyPDF do not run as quickly
|
||||
as some older versions (specifically 6.x and older). This is because OCRmyPDF
|
||||
added image optimization as a postprocessing step, and it is enabled by default.
|
||||
|
||||
Speed
|
||||
=====
|
||||
|
||||
If running OCRmyPDF quickly is your main goal, you can use settings such as:
|
||||
|
||||
* ``--optimize 0`` to disable file size optimization
|
||||
* ``--output-type pdf`` to disable PDF/A generation
|
||||
* ``--fast-web-view 0`` to disable fast web view optimization
|
||||
* ``--skip-big`` to skip large images, if some pages have large images
|
||||
|
||||
You can also avoid:
|
||||
|
||||
* ``--force-ocr``
|
||||
* Image preprocessing
|
||||
@@ -0,0 +1,208 @@
|
||||
=======
|
||||
Plugins
|
||||
=======
|
||||
|
||||
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL
|
||||
NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and
|
||||
"OPTIONAL" in this document are to be interpreted as described in
|
||||
RFC 2119.
|
||||
|
||||
You can use plugins to customize the behavior of OCRmyPDF at certain points of
|
||||
interest.
|
||||
|
||||
Currently, it is possible to:
|
||||
|
||||
- add new command line arguments
|
||||
- override the decision for whether or not to perform OCR on a particular file
|
||||
- modify the image is about to be sent for OCR
|
||||
- modify the page image before it is converted to PDF
|
||||
- replace the Tesseract OCR with another OCR engine that has similar behavior
|
||||
- replace Ghostscript with another PDF to image converter (rasterizer) or
|
||||
PDF/A generator
|
||||
|
||||
OCRmyPDF plugins are based on the Python ``pluggy`` package and conform to its
|
||||
conventions. Note that: plugins installed with as setuptools entrypoints are
|
||||
not checked currently, because OCRmyPDF assumes you may not want to enable
|
||||
plugins for all files.
|
||||
|
||||
Script plugins
|
||||
==============
|
||||
|
||||
Script plugins may be called from the command line, by specifying the name of a file.
|
||||
Script plugins may be convenient for informal or "one-off" plugins, when a certain
|
||||
batch of files needs a special processing step for example.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf --plugin ocrmypdf_example_plugin.py input.pdf output.pdf
|
||||
|
||||
Multiple plugins may be installed by issuing the ``--plugin`` argument multiple times.
|
||||
|
||||
Packaged plugins
|
||||
================
|
||||
|
||||
Installed plugins may be installed into the same virtual environment as OCRmyPDF
|
||||
is installed into. They may be invoked using Python standard module naming.
|
||||
If you are intending to distribute a plugin, please package it.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf --plugin ocrmypdf_fancypants.pockets.contents input.pdf output.pdf
|
||||
|
||||
OCRmyPDF does not automatically import plugins, because the assumption is that
|
||||
plugins affect different files differently and you may not want them activated
|
||||
all the time. The command line or ``ocrmypdf.ocr(plugin='...')`` must call
|
||||
for them.
|
||||
|
||||
Third parties that wish to distribute packages for ocrmypdf should package them
|
||||
as packaged plugins, and these modules should begin with the name ``ocrmypdf_``
|
||||
similar to ``pytest`` packages such as ``pytest-cov`` (the package) and
|
||||
``pytest_cov`` (the module).
|
||||
|
||||
.. note::
|
||||
|
||||
We recommend plugin authors name their plugins with the prefix
|
||||
``ocrmypdf-`` (for the package name on PyPI) and ``ocrmypdf_`` (for the
|
||||
module), just like pytest plugins. At the same time, please make it clear
|
||||
that your package is not official.
|
||||
|
||||
Setuptools plugins
|
||||
==================
|
||||
|
||||
You can also create a plugin that OCRmyPDF will always automatically load if both are
|
||||
installed in the same virtual environment, using a setuptools entrypoint.
|
||||
|
||||
Your package's ``setup.py`` would need to contain the following, for a plugin
|
||||
named ``ocrmypdf-exampleplugin``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# sample ./setup.py file
|
||||
from setuptools import setup
|
||||
|
||||
setup(
|
||||
name="ocrmypdf-exampleplugin",
|
||||
packages=["exampleplugin"],
|
||||
# the following makes a plugin available to pytest
|
||||
entry_points={"ocrmypdf": ["exampleplugin = exampleplugin.pluginmodule"]},
|
||||
)
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
# equivalent setup.cfg
|
||||
[options.entry_points]
|
||||
ocrmypdf =
|
||||
exampleplugin = exampleplugin.pluginmodule
|
||||
|
||||
Plugin requirements
|
||||
===================
|
||||
|
||||
OCRmyPDF generally uses multiple worker processes. When a new worker is started,
|
||||
Python will import all plugins again, including all plugins that were imported earlier.
|
||||
This means that the global state of a plugin in one worker will not be shared with
|
||||
other workers. As such, plugin hook implementations should be stateless, relying
|
||||
only on their inputs. Hook implementations may use their input parameters to
|
||||
to obtain a reference to shared state prepared by another hook implementation.
|
||||
Plugins must expect that other instances of the plugin will be running
|
||||
simultaneously.
|
||||
|
||||
The ``context`` object that is passed to many hooks can be used to share information
|
||||
about a file being worked on. Plugins must write private, plugin-specific data to
|
||||
a subfolder named ``{options.work_folder}/ocrmypdf-plugin-name``. Plugins MAY
|
||||
read and write files in ``options.work_folder``, but should be aware that their
|
||||
semantics are subject to change.
|
||||
|
||||
OCRmyPDF will delete ``options.work_folder`` when it has finished OCRing
|
||||
a file, unless invoked with ``--keep-temporary-files``.
|
||||
|
||||
The documentation for some plugin hooks contain a detailed description of the
|
||||
execution context in which they will be called.
|
||||
|
||||
Plugins should be prepared to work whether executed in worker threads or worker
|
||||
processes. Generally, OCRmyPDF uses processes, but has a semi-hidden threaded
|
||||
argument that simplifies debugging.
|
||||
|
||||
|
||||
Plugin hooks
|
||||
============
|
||||
|
||||
A plugin may provide the following hooks. Hooks must be decorated with
|
||||
``ocrmypdf.hookimpl``, for example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from ocrmpydf import hookimpl
|
||||
|
||||
@hookimpl
|
||||
def add_options(parser):
|
||||
pass
|
||||
|
||||
The following is a complete list of hooks that are available, and when
|
||||
they are called.
|
||||
|
||||
.. _firstresult:
|
||||
|
||||
**Note on firstresult hooks**
|
||||
|
||||
If multiple plugins install implementations for this hook, they will be called in
|
||||
the reverse of the order in which they are installed (i.e., last plugin wins).
|
||||
When each hook implementation is called in order, the first implementation that
|
||||
returns a value other than ``None`` will "win" and prevent execution of all other
|
||||
hooks. As such, you cannot "chain" a series of plugin filters together in this
|
||||
way. Instead, a single hook implementation should be responsible for any such
|
||||
chaining operations.
|
||||
|
||||
Custom command line arguments
|
||||
-----------------------------
|
||||
|
||||
.. autofunction:: ocrmypdf.pluginspec.add_options
|
||||
|
||||
.. autofunction:: ocrmypdf.pluginspec.check_options
|
||||
|
||||
Execution and progress reporting
|
||||
--------------------------------
|
||||
|
||||
.. autoclass: ocrmypdf.pluginspec.Executor
|
||||
:members:
|
||||
|
||||
.. autofunction:: ocrmypdf.pluginspec.get_logging_console
|
||||
|
||||
.. autofunction:: ocrmypdf.pluginspec.get_executor
|
||||
|
||||
.. autofunction:: ocrmypdf.pluginspec.get_progressbar_class
|
||||
|
||||
Applying special behavior before processing
|
||||
-------------------------------------------
|
||||
|
||||
.. autofunction:: ocrmypdf.pluginspec.validate
|
||||
|
||||
PDF page to image
|
||||
-----------------
|
||||
|
||||
.. autofunction:: ocrmypdf.pluginspec.rasterize_pdf_page
|
||||
|
||||
Modifying intermediate images
|
||||
-----------------------------
|
||||
|
||||
.. autofunction:: ocrmypdf.pluginspec.filter_ocr_image
|
||||
|
||||
.. autofunction:: ocrmypdf.pluginspec.filter_page_image
|
||||
|
||||
.. autofunction:: ocrmypdf.pluginspec.filter_pdf_page
|
||||
|
||||
OCR engine
|
||||
----------
|
||||
|
||||
.. autofunction:: ocrmypdf.pluginspec.get_ocr_engine
|
||||
|
||||
.. autoclass:: ocrmypdf.pluginspec.OcrEngine
|
||||
:members:
|
||||
|
||||
.. automethod:: __str__
|
||||
|
||||
.. autoclass:: ocrmypdf.pluginspec.OrientationConfidence
|
||||
|
||||
PDF/A production
|
||||
----------------
|
||||
|
||||
.. autofunction:: ocrmypdf.pluginspec.generate_pdfa
|
||||
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright 2016 findingorder: https://github.com/findingorder
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
# This script must be edited to meet your needs.
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
import ocrmypdf
|
||||
|
||||
# pylint: disable=logging-format-interpolation
|
||||
# pylint: disable=logging-not-lazy
|
||||
|
||||
script_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
print(script_dir + '/batch.py: Start')
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
start_dir = sys.argv[1]
|
||||
else:
|
||||
start_dir = '.'
|
||||
|
||||
if len(sys.argv) > 2:
|
||||
log_file = sys.argv[2]
|
||||
else:
|
||||
log_file = script_dir + '/ocr-tree.log'
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s %(message)s',
|
||||
filename=log_file,
|
||||
filemode='w',
|
||||
)
|
||||
|
||||
ocrmypdf.configure_logging(ocrmypdf.Verbosity.default)
|
||||
|
||||
for dir_name, _subdirs, file_list in os.walk(start_dir):
|
||||
logging.info(dir_name + '\n')
|
||||
os.chdir(dir_name)
|
||||
for filename in file_list:
|
||||
file_ext = os.path.splitext(filename)[1]
|
||||
if file_ext == '.pdf':
|
||||
full_path = dir_name + '/' + filename
|
||||
print(full_path)
|
||||
result = ocrmypdf.ocr(filename, filename, deskew=True)
|
||||
if result == ocrmypdf.ExitCode.already_done_ocr:
|
||||
print("Skipped document because it already contained text")
|
||||
elif result == ocrmypdf.ExitCode.ok:
|
||||
print("OCR complete")
|
||||
logging.info(result)
|
||||
@@ -0,0 +1,120 @@
|
||||
# ocrmypdf completion -*- shell-script -*-
|
||||
|
||||
# Copyright 2019 Frank Pille
|
||||
# Copyright 2020 Alex Willner
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
set -o errexit
|
||||
|
||||
_ocrmypdf()
|
||||
{
|
||||
local cur prev cword words split
|
||||
|
||||
# Homebrew on Macs have version 1.3 of bash-completion which doesn't include - see #502
|
||||
if declare -F _init_completions >/dev/null 2>&1; then
|
||||
_init_completion -s || return
|
||||
else
|
||||
COMPREPLY=()
|
||||
_get_comp_words_by_ref cur prev words cword
|
||||
fi
|
||||
|
||||
if [[ $cur == -* ]]; then
|
||||
COMPREPLY=( $( compgen -W '--language --image-dpi --output-type
|
||||
--sidecar --version --jobs --quiet --verbose --title --author
|
||||
--subject --keywords --rotate-pages --remove-background --deskew
|
||||
--clean --clean-final --unpaper-args --oversample --remove-vectors
|
||||
--threshold --force-ocr --skip-text --redo-ocr
|
||||
--skip-big --jpeg-quality --png-quality --jbig2-lossy
|
||||
--max-image-mpixels --tesseract-config --tesseract-pagesegmode
|
||||
--help --tesseract-oem --pdf-renderer --tesseract-timeout
|
||||
--rotate-pages-threshold --pdfa-image-compression --user-words
|
||||
--user-patterns --keep-temporary-files --output-type
|
||||
--no-progress-bar --pages --fast-web-view' \
|
||||
-- "$cur" ) )
|
||||
return
|
||||
else
|
||||
_filedir
|
||||
return
|
||||
fi
|
||||
|
||||
case $prev in
|
||||
--version|-h|--help)
|
||||
return
|
||||
;;
|
||||
--user-words|--user-patterns|--tesseract-config)
|
||||
_filedir
|
||||
return
|
||||
;;
|
||||
--output-type)
|
||||
COMPREPLY=( $( compgen -W 'pdfa pdf pdfa-1 pdfa-2 pdfa-3' -- \
|
||||
"$cur" ) )
|
||||
return
|
||||
;;
|
||||
--pdf-renderer)
|
||||
COMPREPLY=( $( compgen -W 'auto hocr sandwich' -- "$cur" ) )
|
||||
return
|
||||
;;
|
||||
--pdfa-image-compression)
|
||||
COMPREPLY=( $( compgen -W 'auto jpeg lossless' -- "$cur" ) )
|
||||
return
|
||||
;;
|
||||
-O|--optimize|--tesseract-oem)
|
||||
COMPREPLY=( $( compgen -W '{0..3}' -- "$cur" ) )
|
||||
return
|
||||
;;
|
||||
--jpeg-quality|--png-quality)
|
||||
COMPREPLY=( $( compgen -W '{0..100}' -- "$cur" ) )
|
||||
return
|
||||
;;
|
||||
-l|--language)
|
||||
COMPREPLY=$( command tesseract --list-langs 2>/dev/null )
|
||||
COMPREPLY=( $( compgen -W '${COMPREPLY[@]##*:}' -- "$cur" ) )
|
||||
return
|
||||
;;
|
||||
--image-dpi|--oversample|--skip-big|--max-image-mpixels|\
|
||||
--tesseract-timeout|--rotate-pages-threshold)
|
||||
COMPREPLY=( $( compgen -P "$cur" -W '{0..9}' ) )
|
||||
return
|
||||
;;
|
||||
-j|--jobs)
|
||||
COMPREPLY=( $( compgen -W '{1..'$( _ncpus )'}' -- "$cur" ) )
|
||||
return
|
||||
;;
|
||||
-v|--verbose)
|
||||
COMPREPLY=( $( compgen -W '{0..2}' -- "$cur" ) ) # max level ?
|
||||
return
|
||||
;;
|
||||
--tesseract-pagesegmode)
|
||||
COMPREPLY=( $( compgen -W '{1..13}' -- "$cur" ) )
|
||||
return
|
||||
;;
|
||||
--sidecar|--title|--author|--subject|--keywords|--unpaper-args|--pages|--fast-web-view)
|
||||
# argument required but no completions available
|
||||
return
|
||||
;;
|
||||
esac
|
||||
|
||||
$split && return
|
||||
} &&
|
||||
complete -F _ocrmypdf ocrmypdf
|
||||
|
||||
set +o errexit
|
||||
|
||||
# ex: filetype=sh
|
||||
@@ -0,0 +1,140 @@
|
||||
# Copyright 2020 James R. Barlow
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
complete -c ocrmypdf -x -n '__fish_is_first_arg' -l version
|
||||
complete -c ocrmypdf -x -n '__fish_is_first_arg' -s h -s "?" -l help
|
||||
|
||||
complete -c ocrmypdf -r -l sidecar -d "write OCR to text file"
|
||||
complete -c ocrmypdf -x -s q -l quiet
|
||||
|
||||
complete -c ocrmypdf -s r -l rotate-pages -d "rotate pages to correct orientation"
|
||||
complete -c ocrmypdf -s d -l deskew -d "fix small horizontal alignment skew"
|
||||
complete -c ocrmypdf -s c -l clean -d "clean document images before OCR"
|
||||
complete -c ocrmypdf -s i -l clean-final -d "clean document images and keep result"
|
||||
complete -c ocrmypdf -l remove-vectors -d "don't send vector objects to OCR"
|
||||
complete -c ocrmypdf -l threshold -d "threshold images before OCR"
|
||||
|
||||
complete -c ocrmypdf -s f -l force-ocr -d "OCR documents that already have printable text"
|
||||
complete -c ocrmypdf -s s -l skip-ocr -d "skip OCR on pages that text, otherwise try OCR"
|
||||
complete -c ocrmypdf -l redo-ocr -d "redo OCR on any pages that seem to have OCR already"
|
||||
|
||||
complete -c ocrmypdf -s k -l keep-temporary-files -d "keep temporary files (debug)"
|
||||
|
||||
function __fish_ocrmypdf_languages
|
||||
set langs (tesseract --list-langs ^/dev/null)
|
||||
set arr (string split '\n' $langs)
|
||||
for lang in $arr[2..-1]
|
||||
echo $lang
|
||||
end
|
||||
end
|
||||
complete -c ocrmypdf -x -s l -l language -a '(__fish_ocrmypdf_languages)' -d "language"
|
||||
|
||||
complete -c ocrmypdf -x -l image-dpi -d "assume this DPI if input image DPI is unknown"
|
||||
|
||||
function __fish_ocrmypdf_output_type
|
||||
echo -e "pdfa\t"(_ "output a PDF/A (default)")
|
||||
echo -e "pdf\t"(_ "output a standard PDF")
|
||||
echo -e "pdfa-1\t"(_ "output a PDF/A-1b")
|
||||
echo -e "pdfa-2\t"(_ "output a PDF/A-2b")
|
||||
echo -e "pdfa-3\t"(_ "output a PDF/A-3b")
|
||||
echo -e "none\t"(_ "do not produce an output PDF (for example, if you only care about --sidecar)")
|
||||
end
|
||||
complete -c ocrmypdf -x -l output-type -a '(__fish_ocrmypdf_output_type)' -d "select PDF output options"
|
||||
|
||||
function __fish_ocrmypdf_pdf_renderer
|
||||
echo -e "auto\t"(_ "auto select PDF renderer")
|
||||
echo -e "hocr\t"(_ "use hOCR renderer")
|
||||
echo -e "hocrdebug\t"(_ "uses hOCR renderer in debug mode, showing recognized text")
|
||||
echo -e "sandwich\t"(_ "use sandwich renderer")
|
||||
end
|
||||
complete -c ocrmypdf -x -l pdf-renderer -a '(__fish_ocrmypdf_pdf_renderer)' -d "select PDF renderer options"
|
||||
|
||||
function __fish_ocrmypdf_optimize
|
||||
echo -e "0\t"(_ "do not optimize")
|
||||
echo -e "1\t"(_ "do safe, lossless optimizations (default)")
|
||||
echo -e "2\t"(_ "do some lossy optimizations")
|
||||
echo -e "3\t"(_ "do aggressive lossy optimizations (including lossy JBIG2)")
|
||||
end
|
||||
complete -c ocrmypdf -x -s O -l optimize -a '(__fish_ocrmypdf_optimize)' -d "select optimization level"
|
||||
|
||||
function __fish_ocrmypdf_verbose
|
||||
echo -e "0\t"(_ "standard output messages")
|
||||
echo -e "1\t"(_ "troubleshooting output messages")
|
||||
echo -e "2\t"(_ "debugging output messages")
|
||||
end
|
||||
complete -c ocrmypdf -x -s v -l verbose -a '(__fish_ocrmypdf_verbose)' -d "set verbosity level"
|
||||
|
||||
complete -c ocrmypdf -x -l no-progress-bar -d "disable the progress bar"
|
||||
|
||||
function __fish_ocrmypdf_pdfa_compression
|
||||
echo -e "auto\t"(_ "let Ghostscript decide how to compress images")
|
||||
echo -e "jpeg\t"(_ "convert color and grayscale images to JPEG")
|
||||
echo -e "lossless\t"(_ "convert color and grayscale images to lossless (PNG)")
|
||||
end
|
||||
complete -c ocrmypdf -x -l pdfa-image-compression -a '(__fish_ocrmypdf_pdfa_compression)' -d "set PDF/A image compression options"
|
||||
|
||||
complete -c ocrmypdf -x -s j -l jobs -d "how many worker processes to use"
|
||||
complete -c ocrmypdf -x -l title -d "set metadata"
|
||||
complete -c ocrmypdf -x -l author -d "set metadata"
|
||||
complete -c ocrmypdf -x -l subject -d "set metadata"
|
||||
complete -c ocrmypdf -x -l keywords -d "set metadata"
|
||||
complete -c ocrmypdf -x -l oversample -d "oversample images to this DPI"
|
||||
complete -c ocrmypdf -x -l skip-big -d "skip OCR on pages larger than this many MPixels"
|
||||
|
||||
complete -c ocrmypdf -x -l jpeg-quality -d "JPEG quality [0..100]"
|
||||
complete -c ocrmypdf -x -l png-quality -d "PNG quality [0..100]"
|
||||
complete -c ocrmypdf -x -l jbig2-lossy -d "enable lossy JBIG2 (see docs)"
|
||||
complete -c ocrmypdf -x -l max-image-mpixels -d "image decompression bomb threshold"
|
||||
complete -c ocrmypdf -x -l pages -d "apply OCR to only the specified pages"
|
||||
complete -c ocrmypdf -x -l tesseract-config -d "set custom tesseract config file"
|
||||
|
||||
function __fish_ocrmypdf_tesseract_pagesegmode
|
||||
echo -e "0\t"(_ "orientation and script detection (OSD) only")
|
||||
echo -e "1\t"(_ "automatic page segmentation with OSD")
|
||||
echo -e "2\t"(_ "automatic page segmentation, but no OSD, or OCR")
|
||||
echo -e "3\t"(_ "fully automatic page segmentation, but no OSD (default)")
|
||||
echo -e "4\t"(_ "assume a single column of text of variable sizes")
|
||||
echo -e "5\t"(_ "assume a single uniform block of vertically aligned text")
|
||||
echo -e "6\t"(_ "assume a single uniform block of text")
|
||||
echo -e "7\t"(_ "treat the image as a single text line")
|
||||
echo -e "8\t"(_ "treat the image as a single word")
|
||||
echo -e "9\t"(_ "treat the image as a single word in a circle")
|
||||
echo -e "10\t"(_ "treat the image as a single character")
|
||||
echo -e "11\t"(_ "sparse text - find as much text as possible in no particular order")
|
||||
echo -e "12\t"(_ "sparse text with OSD")
|
||||
echo -e "13\t"(_ "raw line - treat the image as a single text line")
|
||||
end
|
||||
complete -c ocrmypdf -x -l tesseract-pagesegmode -a '(__fish_ocrmypdf_tesseract_pagesegmode)' -d "set tesseract --psm"
|
||||
|
||||
function __fish_ocrmypdf_tesseract_oem
|
||||
echo -e "0\t"(_ "legacy engine only")
|
||||
echo -e "1\t"(_ "neural nets LSTM engine only")
|
||||
echo -e "2\t"(_ "legacy + LSTM engines")
|
||||
echo -e "3\t"(_ "default, based on what is available")
|
||||
end
|
||||
complete -c ocrmypdf -x -l tesseract-oem -a '(__fish_ocrmypdf_tesseract_oem)' -d "set tesseract --oem"
|
||||
complete -c ocrmypdf -x -l tesseract-timeout -d "maximum number of seconds to wait for OCR"
|
||||
complete -c ocrmypdf -x -l rotate-pages-threshold -d "page rotation confidence"
|
||||
|
||||
complete -c ocrmypdf -r -l user-words -d "specify location of user words file"
|
||||
complete -c ocrmypdf -r -l user-patterns -d "specify location of user patterns file"
|
||||
complete -c ocrmypdf -x -l fast-web-view -d "if file size if above this amount in MB, linearize PDF"
|
||||
|
||||
complete -c ocrmypdf -x -a "(__fish_complete_suffix .pdf; __fish_complete_suffix .PDF; __fish_complete_suffix .jpg; __fish_complete_suffix .png)"
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
version: "3.3"
|
||||
services:
|
||||
ocrmypdf:
|
||||
restart: always
|
||||
container_name: ocrmypdf
|
||||
image: jbarlow83/ocrmypdf
|
||||
volumes:
|
||||
- "/media/scan:/input"
|
||||
- "/mnt/scan:/output"
|
||||
environment:
|
||||
- OCR_OUTPUT_DIRECTORY_YEAR_MONTH=0
|
||||
user: "<SET TO YOUR USER ID>:<SET TO YOUR GROUP ID>"
|
||||
entrypoint: python3
|
||||
command: watcher.py
|
||||
@@ -0,0 +1,84 @@
|
||||
# © 2020 James R Barlow: https://github.com/jbarlow83
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
"""
|
||||
An example of an OCRmyPDF plugin.
|
||||
|
||||
This plugin adds two new command line arguments
|
||||
--grayscale-ocr: converts the image to grayscale before performing OCR on it
|
||||
(This is occasionally useful for images whose color confounds OCR. It only
|
||||
affects the image shown to OCR. The image is not saved.)
|
||||
--mono-page: converts pages all pages in the output file to black and white
|
||||
|
||||
To use this from the command line:
|
||||
ocrmypdf --plugin path/to/example_plugin.py --mono-page input.pdf output.pdf
|
||||
|
||||
To use this as an API:
|
||||
import ocrmypdf
|
||||
ocrmypdf.ocr('input.pdf', 'output.pdf',
|
||||
plugins=['path/to/example_plugin.py'], mono_page=True
|
||||
)
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from ocrmypdf import hookimpl
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@hookimpl
|
||||
def add_options(parser):
|
||||
parser.add_argument('--grayscale-ocr', action='store_true')
|
||||
parser.add_argument('--mono-page', action='store_true')
|
||||
|
||||
|
||||
@hookimpl
|
||||
def prepare(options):
|
||||
pass
|
||||
|
||||
|
||||
@hookimpl
|
||||
def validate(pdfinfo, options):
|
||||
pass
|
||||
|
||||
|
||||
@hookimpl
|
||||
def filter_ocr_image(page, image):
|
||||
if page.options.grayscale_ocr:
|
||||
log.info("graying")
|
||||
return image.convert('L')
|
||||
return image
|
||||
|
||||
|
||||
@hookimpl
|
||||
def filter_page_image(page, image_filename):
|
||||
if page.options.mono_page:
|
||||
with Image.open(image_filename) as im:
|
||||
im = im.convert('1')
|
||||
im.save(image_filename)
|
||||
return image_filename
|
||||
else:
|
||||
output = image_filename.with_suffix('.jpg')
|
||||
with Image.open(image_filename) as im:
|
||||
im.save(output)
|
||||
return output
|
||||
@@ -0,0 +1,92 @@
|
||||
#!/bin/env python3
|
||||
# Copyright 2017 github.com/Enantiomerie
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
# This script must be edited to meet your needs.
|
||||
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
# pylint: disable=logging-format-interpolation
|
||||
# pylint: disable=logging-not-lazy
|
||||
|
||||
script_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
timestamp = time.strftime("%Y-%m-%d-%H%M_")
|
||||
log_file = script_dir + '/' + timestamp + 'ocrmypdf.log'
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s %(message)s',
|
||||
filename=log_file,
|
||||
filemode='w',
|
||||
)
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
start_dir = sys.argv[1]
|
||||
else:
|
||||
start_dir = '.'
|
||||
|
||||
for dir_name, _subdirs, file_list in os.walk(start_dir):
|
||||
logging.info(dir_name)
|
||||
os.chdir(dir_name)
|
||||
for filename in file_list:
|
||||
file_stem, file_ext = os.path.splitext(filename)
|
||||
if file_ext != '.pdf':
|
||||
continue
|
||||
full_path = os.path.join(dir_name, filename)
|
||||
timestamp_ocr = time.strftime("%Y-%m-%d-%H%M_OCR_")
|
||||
filename_ocr = timestamp_ocr + file_stem + '.pdf'
|
||||
# create string for pdf processing
|
||||
# the script is processed as root user via chron
|
||||
cmd = [
|
||||
'docker',
|
||||
'run',
|
||||
'--rm',
|
||||
'-i',
|
||||
'jbarlow83/ocrmypdf',
|
||||
'--deskew',
|
||||
'-',
|
||||
'-',
|
||||
]
|
||||
logging.info(cmd)
|
||||
full_path_ocr = os.path.join(dir_name, filename_ocr)
|
||||
with open(filename, 'rb') as input_file, open(
|
||||
full_path_ocr, 'wb'
|
||||
) as output_file:
|
||||
proc = subprocess.run(
|
||||
cmd,
|
||||
stdin=input_file,
|
||||
stdout=output_file,
|
||||
stderr=subprocess.PIPE,
|
||||
check=False,
|
||||
text=True,
|
||||
errors='ignore',
|
||||
)
|
||||
logging.info(proc.stderr)
|
||||
os.chmod(full_path_ocr, 0o664)
|
||||
os.chmod(full_path, 0o664)
|
||||
full_path_ocr_archive = sys.argv[2]
|
||||
full_path_archive = sys.argv[2] + '/no_ocr'
|
||||
shutil.move(full_path_ocr, full_path_ocr_archive)
|
||||
shutil.move(full_path, full_path_archive)
|
||||
logging.info('Finished.\n')
|
||||
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright (C) 2019 Ian Alexander: https://github.com/ianalexander
|
||||
# Copyright (C) 2020 James R Barlow: https://github.com/jbarlow83
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import pikepdf
|
||||
from watchdog.events import PatternMatchingEventHandler
|
||||
from watchdog.observers import Observer
|
||||
from watchdog.observers.polling import PollingObserver
|
||||
|
||||
import ocrmypdf
|
||||
|
||||
# pylint: disable=logging-format-interpolation
|
||||
|
||||
|
||||
def getenv_bool(name: str, default: str = 'False'):
|
||||
return os.getenv(name, default).lower() in ('true', 'yes', 'y', '1')
|
||||
|
||||
|
||||
INPUT_DIRECTORY = os.getenv('OCR_INPUT_DIRECTORY', '/input')
|
||||
OUTPUT_DIRECTORY = os.getenv('OCR_OUTPUT_DIRECTORY', '/output')
|
||||
OUTPUT_DIRECTORY_YEAR_MONTH = getenv_bool('OCR_OUTPUT_DIRECTORY_YEAR_MONTH')
|
||||
ON_SUCCESS_DELETE = getenv_bool('OCR_ON_SUCCESS_DELETE')
|
||||
DESKEW = getenv_bool('OCR_DESKEW')
|
||||
OCR_JSON_SETTINGS = json.loads(os.getenv('OCR_JSON_SETTINGS', '{}'))
|
||||
POLL_NEW_FILE_SECONDS = int(os.getenv('OCR_POLL_NEW_FILE_SECONDS', '1'))
|
||||
USE_POLLING = getenv_bool('OCR_USE_POLLING')
|
||||
LOGLEVEL = os.getenv('OCR_LOGLEVEL', 'INFO')
|
||||
PATTERNS = ['*.pdf', '*.PDF']
|
||||
|
||||
log = logging.getLogger('ocrmypdf-watcher')
|
||||
|
||||
|
||||
def get_output_dir(root, basename):
|
||||
if OUTPUT_DIRECTORY_YEAR_MONTH:
|
||||
today = datetime.today()
|
||||
output_directory_year_month = (
|
||||
Path(root) / str(today.year) / f'{today.month:02d}'
|
||||
)
|
||||
if not output_directory_year_month.exists():
|
||||
output_directory_year_month.mkdir(parents=True, exist_ok=True)
|
||||
output_path = Path(output_directory_year_month) / basename
|
||||
else:
|
||||
output_path = Path(OUTPUT_DIRECTORY) / basename
|
||||
return output_path
|
||||
|
||||
|
||||
def wait_for_file_ready(file_path):
|
||||
# This loop waits to make sure that the file is completely loaded on
|
||||
# disk before attempting to read. Docker sometimes will publish the
|
||||
# watchdog event before the file is actually fully on disk, causing
|
||||
# pikepdf to fail.
|
||||
|
||||
retries = 5
|
||||
while retries:
|
||||
try:
|
||||
pdf = pikepdf.open(file_path)
|
||||
except (FileNotFoundError, pikepdf.PdfError) as e:
|
||||
log.info(f"File {file_path} is not ready yet")
|
||||
log.debug("Exception was", exc_info=e)
|
||||
time.sleep(POLL_NEW_FILE_SECONDS)
|
||||
retries -= 1
|
||||
else:
|
||||
pdf.close()
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def execute_ocrmypdf(file_path):
|
||||
file_path = Path(file_path)
|
||||
output_path = get_output_dir(OUTPUT_DIRECTORY, file_path.name)
|
||||
|
||||
log.info("-" * 20)
|
||||
log.info(f'New file: {file_path}. Waiting until fully loaded...')
|
||||
if not wait_for_file_ready(file_path):
|
||||
log.info(f"Gave up waiting for {file_path} to become ready")
|
||||
return
|
||||
log.info(f'Attempting to OCRmyPDF to: {output_path}')
|
||||
exit_code = ocrmypdf.ocr(
|
||||
input_file=file_path,
|
||||
output_file=output_path,
|
||||
deskew=DESKEW,
|
||||
**OCR_JSON_SETTINGS,
|
||||
)
|
||||
if exit_code == 0 and ON_SUCCESS_DELETE:
|
||||
log.info(f'OCR is done. Deleting: {file_path}')
|
||||
file_path.unlink()
|
||||
else:
|
||||
log.info('OCR is done')
|
||||
|
||||
|
||||
class HandleObserverEvent(PatternMatchingEventHandler):
|
||||
def on_any_event(self, event):
|
||||
if event.event_type in ['created']:
|
||||
execute_ocrmypdf(event.src_path)
|
||||
|
||||
|
||||
def main():
|
||||
ocrmypdf.configure_logging(
|
||||
verbosity=(
|
||||
ocrmypdf.Verbosity.default
|
||||
if LOGLEVEL != 'DEBUG'
|
||||
else ocrmypdf.Verbosity.debug
|
||||
),
|
||||
manage_root_logger=True,
|
||||
)
|
||||
log.setLevel(LOGLEVEL)
|
||||
log.info(
|
||||
f"Starting OCRmyPDF watcher with config:\n"
|
||||
f"Input Directory: {INPUT_DIRECTORY}\n"
|
||||
f"Output Directory: {OUTPUT_DIRECTORY}\n"
|
||||
f"Output Directory Year & Month: {OUTPUT_DIRECTORY_YEAR_MONTH}"
|
||||
)
|
||||
log.debug(
|
||||
f"INPUT_DIRECTORY: {INPUT_DIRECTORY}\n"
|
||||
f"OUTPUT_DIRECTORY: {OUTPUT_DIRECTORY}\n"
|
||||
f"OUTPUT_DIRECTORY_YEAR_MONTH: {OUTPUT_DIRECTORY_YEAR_MONTH}\n"
|
||||
f"ON_SUCCESS_DELETE: {ON_SUCCESS_DELETE}\n"
|
||||
f"DESKEW: {DESKEW}\n"
|
||||
f"ARGS: {OCR_JSON_SETTINGS}\n"
|
||||
f"POLL_NEW_FILE_SECONDS: {POLL_NEW_FILE_SECONDS}\n"
|
||||
f"USE_POLLING: {USE_POLLING}\n"
|
||||
f"LOGLEVEL: {LOGLEVEL}"
|
||||
)
|
||||
|
||||
if 'input_file' in OCR_JSON_SETTINGS or 'output_file' in OCR_JSON_SETTINGS:
|
||||
log.error('OCR_JSON_SETTINGS should not specify input file or output file')
|
||||
sys.exit(1)
|
||||
|
||||
handler = HandleObserverEvent(patterns=PATTERNS)
|
||||
if USE_POLLING:
|
||||
observer = PollingObserver()
|
||||
else:
|
||||
observer = Observer()
|
||||
observer.schedule(handler, INPUT_DIRECTORY, recursive=True)
|
||||
observer.start()
|
||||
try:
|
||||
while True:
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
observer.stop()
|
||||
observer.join()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env python3
|
||||
# webservice.py wrapper for OCRmyPDF
|
||||
# Copyright (C) 2019 James R. Barlow: github.com/jbarlow83
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""This is a simple web service/HTTP wrapper for OCRmyPDF
|
||||
|
||||
This may be more convenient than the command line tool for some Docker users.
|
||||
Note that OCRmyPDF uses Ghostscript, which is licensed under AGPLv3+. While
|
||||
OCRmyPDF is under GPLv3, this file is distributed under the Affero GPLv3+ license,
|
||||
to emphasize that SaaS deployments should make sure they comply with
|
||||
Ghostscript's license as well as OCRmyPDF's.
|
||||
"""
|
||||
|
||||
import os
|
||||
import shlex
|
||||
from subprocess import PIPE, run
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
from flask import Flask, Response, request, send_from_directory
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
app = Flask(__name__)
|
||||
app.secret_key = "secret"
|
||||
app.config['MAX_CONTENT_LENGTH'] = 50_000_000
|
||||
app.config.from_envvar("OCRMYPDF_WEBSERVICE_SETTINGS", silent=True)
|
||||
|
||||
ALLOWED_EXTENSIONS = {"pdf"}
|
||||
|
||||
|
||||
def allowed_file(filename):
|
||||
return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS
|
||||
|
||||
|
||||
def do_ocrmypdf(file):
|
||||
uploaddir = TemporaryDirectory(prefix="ocrmypdf-upload")
|
||||
downloaddir = TemporaryDirectory(prefix="ocrmypdf-download")
|
||||
|
||||
filename = secure_filename(file.filename)
|
||||
up_file = os.path.join(uploaddir.name, filename)
|
||||
file.save(up_file)
|
||||
|
||||
down_file = os.path.join(downloaddir.name, filename)
|
||||
|
||||
cmd_args = [arg for arg in shlex.split(request.form["params"])]
|
||||
if "--sidecar" in cmd_args:
|
||||
return Response("--sidecar not supported", 501, mimetype='text/plain')
|
||||
|
||||
ocrmypdf_args = ["ocrmypdf", *cmd_args, up_file, down_file]
|
||||
proc = run(ocrmypdf_args, stdout=PIPE, stderr=PIPE, encoding="utf-8")
|
||||
if proc.returncode != 0:
|
||||
stderr = proc.stderr
|
||||
return Response(stderr, 400, mimetype='text/plain')
|
||||
|
||||
return send_from_directory(downloaddir.name, filename)
|
||||
|
||||
|
||||
@app.route("/", methods=["GET", "POST"])
|
||||
def upload_file():
|
||||
if request.method == "POST":
|
||||
if "file" not in request.files:
|
||||
return Response("No file in POST", 400, mimetype='text/plain')
|
||||
file = request.files["file"]
|
||||
if file.filename == "":
|
||||
return Response("Empty filename", 400, mimetype='text/plain')
|
||||
if not allowed_file(file.filename):
|
||||
return Response("Invalid filename", 400, mimetype='text/plain')
|
||||
if file and allowed_file(file.filename):
|
||||
return do_ocrmypdf(file)
|
||||
return Response("Some other problem", 400, mimetype='text/plain')
|
||||
|
||||
return """
|
||||
<!doctype html>
|
||||
<title>OCRmyPDF webservice</title>
|
||||
<h1>Upload a PDF (debug UI)</h1>
|
||||
<form method=post enctype=multipart/form-data>
|
||||
<label for="args">Command line parameters</label>
|
||||
<input type=textbox name=params>
|
||||
<label for="file">File to upload</label>
|
||||
<input type=file name=file>
|
||||
<input type=submit value=Upload>
|
||||
</form>
|
||||
<h4>Notice</h2>
|
||||
<div style="font-size: 70%; max-width: 34em;">
|
||||
<p>This is a webservice wrapper for OCRmyPDF.</p>
|
||||
<p>Copyright 2019 James R. Barlow</p>
|
||||
<p>This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
</p>
|
||||
<p>This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
</p>
|
||||
<p>
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
</p>
|
||||
</div>
|
||||
"""
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host='0.0.0.0', port=5000)
|
||||
@@ -1,34 +0,0 @@
|
||||
from enum import IntEnum
|
||||
import os
|
||||
from collections.abc import Iterable
|
||||
import pkg_resources
|
||||
|
||||
VERSION = pkg_resources.get_distribution('ocrmypdf').version
|
||||
|
||||
|
||||
class ExitCode(IntEnum):
|
||||
ok = 0
|
||||
bad_args = 1
|
||||
input_file = 2
|
||||
missing_dependency = 3
|
||||
invalid_output_pdf = 4
|
||||
file_access_error = 5
|
||||
already_done_ocr = 6
|
||||
child_process_error = 7
|
||||
encrypted_pdf = 8
|
||||
other_error = 15
|
||||
ctrl_c = 130
|
||||
|
||||
|
||||
def get_program(name):
|
||||
envvar = 'OCRMYPDF_' + name.upper()
|
||||
return os.environ.get(envvar, name)
|
||||
|
||||
|
||||
def page_number(input_file):
|
||||
return int(os.path.basename(input_file)[0:6])
|
||||
|
||||
|
||||
def is_iterable_notstr(thing):
|
||||
return isinstance(thing, Iterable) and not isinstance(thing, str)
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# © 2015 James R. Barlow: github.com/jbarlow83
|
||||
|
||||
from tempfile import NamedTemporaryFile
|
||||
from subprocess import Popen, PIPE, check_call
|
||||
from shutil import copy
|
||||
from . import get_program
|
||||
from .pdfa import SRGB_ICC_PROFILE
|
||||
|
||||
|
||||
def rasterize_pdf(input_file, output_file, xres, yres, raster_device, log,
|
||||
pageno=1):
|
||||
with NamedTemporaryFile(delete=True) as tmp:
|
||||
args_gs = [
|
||||
get_program('gs'),
|
||||
'-dQUIET',
|
||||
'-dSAFER',
|
||||
'-dBATCH',
|
||||
'-dNOPAUSE',
|
||||
'-sDEVICE=%s' % raster_device,
|
||||
'-dFirstPage=%i' % pageno,
|
||||
'-dLastPage=%i' % pageno,
|
||||
'-o', tmp.name,
|
||||
'-r{0}x{1}'.format(str(round(xres)), str(round(yres))),
|
||||
input_file
|
||||
]
|
||||
|
||||
p = Popen(args_gs, close_fds=True, stdout=PIPE, stderr=PIPE,
|
||||
universal_newlines=True)
|
||||
stdout, stderr = p.communicate()
|
||||
if stdout:
|
||||
if 'error' in stdout:
|
||||
log.error(stdout) # Ghostscript puts errors in stdout
|
||||
else:
|
||||
log.debug(stdout)
|
||||
if stderr:
|
||||
log.error(stderr)
|
||||
|
||||
if p.returncode == 0:
|
||||
copy(tmp.name, output_file)
|
||||
else:
|
||||
log.error('Ghostscript rendering failed')
|
||||
|
||||
|
||||
def generate_pdfa(pdf_pages, output_file, threads=1):
|
||||
with NamedTemporaryFile(delete=True) as gs_pdf:
|
||||
args_gs = [
|
||||
get_program("gs"),
|
||||
"-dQUIET",
|
||||
"-dBATCH",
|
||||
"-dNOPAUSE",
|
||||
'-dNumRenderingThreads=' + str(threads),
|
||||
"-sDEVICE=pdfwrite",
|
||||
"-dAutoRotatePages=/None",
|
||||
"-sColorConversionStrategy=/RGB",
|
||||
"-sProcessColorModel=DeviceRGB",
|
||||
"-dJPEGQ=95",
|
||||
"-dPDFA=2",
|
||||
"-sPDFACompatibilityPolicy=2",
|
||||
"-sOutputFile=" + gs_pdf.name,
|
||||
]
|
||||
args_gs.extend(pdf_pages)
|
||||
check_call(args_gs)
|
||||
copy(gs_pdf.name, output_file)
|
||||
@@ -1,230 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
##############################################################################
|
||||
# Copyright (c) 2013-14: fritz-hh from Github
|
||||
# (https://github.com/fritz-hh)
|
||||
#
|
||||
# Copyright (c) 2010: Jonathan Brinley from Github
|
||||
# (https://github.com/jbrinley/HocrConverter)
|
||||
# Initial version by Jonathan Brinley, jonathanbrinley@gmail.com
|
||||
##############################################################################
|
||||
from reportlab.pdfgen.canvas import Canvas
|
||||
from reportlab.lib.units import inch
|
||||
from xml.etree import ElementTree
|
||||
from PIL import Image
|
||||
from collections import namedtuple
|
||||
import re
|
||||
import argparse
|
||||
|
||||
|
||||
Rect = namedtuple('Rect', ['x1', 'y1', 'x2', 'y2'])
|
||||
|
||||
|
||||
class HocrTransformError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class HocrTransform():
|
||||
|
||||
"""
|
||||
A class for converting documents from the hOCR format.
|
||||
For details of the hOCR format, see:
|
||||
http://docs.google.com/View?docid=dfxcv4vc_67g844kf
|
||||
"""
|
||||
|
||||
def __init__(self, hocrFileName, dpi):
|
||||
self.dpi = dpi
|
||||
self.boxPattern = re.compile(r'bbox((\s+\d+){4})')
|
||||
|
||||
self.hocr = ElementTree.parse(hocrFileName)
|
||||
|
||||
# if the hOCR file has a namespace, ElementTree requires its use to
|
||||
# find elements
|
||||
matches = re.match(r'({.*})html', self.hocr.getroot().tag)
|
||||
self.xmlns = ''
|
||||
if matches:
|
||||
self.xmlns = matches.group(1)
|
||||
|
||||
# get dimension in pt (not pixel!!!!) of the OCRed image
|
||||
self.width, self.height = None, None
|
||||
for div in self.hocr.findall(
|
||||
".//%sdiv[@class='ocr_page']" % (self.xmlns)):
|
||||
coords = self.element_coordinates(div)
|
||||
pt_coords = self.pt_from_pixel(coords)
|
||||
self.width = pt_coords.x2 - pt_coords.x1
|
||||
self.height = pt_coords.y2 - pt_coords.y1
|
||||
# there shouldn't be more than one, and if there is, we don't want
|
||||
# it
|
||||
break
|
||||
if self.width is None or self.height is None:
|
||||
raise HocrTransformError("hocr file is missing page dimensions")
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
Return the textual content of the HTML body
|
||||
"""
|
||||
if self.hocr is None:
|
||||
return ''
|
||||
body = self.hocr.find(".//%sbody" % (self.xmlns))
|
||||
if body:
|
||||
return self._get_element_text(body)
|
||||
else:
|
||||
return ''
|
||||
|
||||
def _get_element_text(self, element):
|
||||
"""
|
||||
Return the textual content of the element and its children
|
||||
"""
|
||||
text = ''
|
||||
if element.text is not None:
|
||||
text += element.text
|
||||
for child in element.getchildren():
|
||||
text += self._get_element_text(child)
|
||||
if element.tail is not None:
|
||||
text += element.tail
|
||||
return text
|
||||
|
||||
def element_coordinates(self, element):
|
||||
"""
|
||||
Returns a tuple containing the coordinates of the bounding box around
|
||||
an element
|
||||
"""
|
||||
out = (0, 0, 0, 0)
|
||||
if 'title' in element.attrib:
|
||||
matches = self.boxPattern.search(element.attrib['title'])
|
||||
if matches:
|
||||
coords = matches.group(1).split()
|
||||
out = Rect._make(int(coords[n]) for n in range(4))
|
||||
return out
|
||||
|
||||
def pt_from_pixel(self, pxl):
|
||||
"""
|
||||
Returns the quantity in PDF units (pt) given quantity in pixels
|
||||
"""
|
||||
return Rect._make(
|
||||
(c / self.dpi * inch) for c in pxl)
|
||||
|
||||
def replace_unsupported_chars(self, s):
|
||||
"""
|
||||
Given an input string, returns the corresponding string that:
|
||||
- is available in the helvetica facetype
|
||||
- does not contain any ligature (to allow easy search in the PDF file)
|
||||
"""
|
||||
# The 'u' before the character to replace indicates that it is a
|
||||
# unicode character
|
||||
s = s.replace(u"fl", "fl")
|
||||
s = s.replace(u"fi", "fi")
|
||||
return s
|
||||
|
||||
def to_pdf(self, outFileName, imageFileName=None, showBoundingboxes=False,
|
||||
fontname="Helvetica", invisibleText=False):
|
||||
"""
|
||||
Creates a PDF file with an image superimposed on top of the text.
|
||||
Text is positioned according to the bounding box of the lines in
|
||||
the hOCR file.
|
||||
The image need not be identical to the image used to create the hOCR
|
||||
file.
|
||||
It can have a lower resolution, different color mode, etc.
|
||||
"""
|
||||
# create the PDF file
|
||||
# page size in points (1/72 in.)
|
||||
pdf = Canvas(
|
||||
outFileName, pagesize=(self.width, self.height), pageCompression=1)
|
||||
|
||||
# draw bounding box for each paragraph
|
||||
# light blue for bounding box of paragraph
|
||||
pdf.setStrokeColorRGB(0, 1, 1)
|
||||
# light blue for bounding box of paragraph
|
||||
pdf.setFillColorRGB(0, 1, 1)
|
||||
pdf.setLineWidth(0) # no line for bounding box
|
||||
for elem in self.hocr.findall(
|
||||
".//%sp[@class='%s']" % (self.xmlns, "ocr_par")):
|
||||
|
||||
elemtxt = self._get_element_text(elem).rstrip()
|
||||
if len(elemtxt) == 0:
|
||||
continue
|
||||
|
||||
pxl_coords = self.element_coordinates(elem)
|
||||
pt = self.pt_from_pixel(pxl_coords)
|
||||
|
||||
# draw the bbox border
|
||||
if showBoundingboxes:
|
||||
pdf.rect(
|
||||
pt.x1, self.height - pt.y2, pt.x2 - pt.x1, pt.y2 - pt.y1,
|
||||
fill=1)
|
||||
|
||||
# check if element with class 'ocrx_word' are available
|
||||
# otherwise use 'ocr_line' as fallback
|
||||
elemclass = "ocr_line"
|
||||
if self.hocr.find(
|
||||
".//%sspan[@class='ocrx_word']" % (self.xmlns)) is not None:
|
||||
elemclass = "ocrx_word"
|
||||
|
||||
# itterate all text elements
|
||||
# light green for bounding box of word/line
|
||||
pdf.setStrokeColorRGB(1, 0, 0)
|
||||
pdf.setLineWidth(0.5) # bounding box line width
|
||||
pdf.setDash(6, 3) # bounding box is dashed
|
||||
pdf.setFillColorRGB(0, 0, 0) # text in black
|
||||
for elem in self.hocr.findall(
|
||||
".//%sspan[@class='%s']" % (self.xmlns, elemclass)):
|
||||
|
||||
elemtxt = self._get_element_text(elem).rstrip()
|
||||
|
||||
elemtxt = self.replace_unsupported_chars(elemtxt)
|
||||
|
||||
if len(elemtxt) == 0:
|
||||
continue
|
||||
|
||||
pxl_coords = self.element_coordinates(elem)
|
||||
pt = self.pt_from_pixel(pxl_coords)
|
||||
|
||||
# draw the bbox border
|
||||
if showBoundingboxes:
|
||||
pdf.rect(
|
||||
pt.x1, self.height - pt.y2, pt.x2 - pt.x1, pt.y2 - pt.y1,
|
||||
fill=0)
|
||||
|
||||
text = pdf.beginText()
|
||||
fontsize = pt.y2 - pt.y1
|
||||
text.setFont(fontname, fontsize)
|
||||
if invisibleText:
|
||||
text.setTextRenderMode(3) # Invisible (indicates OCR text)
|
||||
|
||||
# set cursor to bottom left corner of bbox (adjust for dpi)
|
||||
text.setTextOrigin(pt.x1, self.height - pt.y2)
|
||||
|
||||
# scale the width of the text to fill the width of the bbox
|
||||
text.setHorizScale(
|
||||
100 * (pt.x2 - pt.x1) / pdf.stringWidth(
|
||||
elemtxt, fontname, fontsize))
|
||||
|
||||
# write the text to the page
|
||||
text.textLine(elemtxt)
|
||||
pdf.drawText(text)
|
||||
|
||||
# put the image on the page, scaled to fill the page
|
||||
if imageFileName is not None:
|
||||
pdf.drawImage(imageFileName, 0, 0,
|
||||
width=self.width, height=self.height)
|
||||
|
||||
# finish up the page and save it
|
||||
pdf.showPage()
|
||||
pdf.save()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description='Convert hocr file to PDF')
|
||||
parser.add_argument('-b', '--boundingboxes', action="store_true",
|
||||
default=False, help='Show bounding boxes borders')
|
||||
parser.add_argument('-r', '--resolution', type=int,
|
||||
default=300,
|
||||
help='Resolution of the image that was OCRed')
|
||||
parser.add_argument('-i', '--image', default=None,
|
||||
help='Path to the image to be placed above the text')
|
||||
parser.add_argument('hocrfile', help='Path to the hocr file to be parsed')
|
||||
parser.add_argument(
|
||||
'outputfile', help='Path to the PDF file to be generated')
|
||||
args = parser.parse_args()
|
||||
|
||||
hocr = HocrTransform(args.hocrfile, args.resolution)
|
||||
hocr.to_pdf(args.outputfile, args.image, args.boundingboxes)
|
||||
@@ -1,592 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# © 2013-16: jbarlow83 from Github (https://github.com/jbarlow83)
|
||||
#
|
||||
# Python FFI wrapper for Leptonica library
|
||||
|
||||
from __future__ import print_function, absolute_import, division
|
||||
import argparse
|
||||
import sys
|
||||
import os
|
||||
import logging
|
||||
from tempfile import TemporaryFile
|
||||
from ctypes.util import find_library
|
||||
from .lib._leptonica import ffi
|
||||
from functools import lru_cache
|
||||
from enum import Enum
|
||||
|
||||
lept = ffi.dlopen(find_library('lept'))
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def stderr(*objs):
|
||||
"""Python 2/3 compatible print to stderr.
|
||||
"""
|
||||
print("leptonica.py:", *objs, file=sys.stderr)
|
||||
|
||||
|
||||
class LeptonicaErrorTrap(object):
|
||||
"""Context manager to trap errors reported by Leptonica.
|
||||
|
||||
Leptonica's error return codes are unreliable to the point of being
|
||||
almost useless. It does, however, write errors to stderr provided that is
|
||||
not disabled at its compile time. Fortunately this is done using error
|
||||
macros so it is very self-consistent.
|
||||
|
||||
This context manager redirects stderr to a temporary file which is then
|
||||
read and parsed for error messages. As a side benefit, debug messages
|
||||
from Leptonica are also suppressed.
|
||||
|
||||
"""
|
||||
def __enter__(self):
|
||||
from io import UnsupportedOperation
|
||||
self.tmpfile = TemporaryFile()
|
||||
|
||||
# Save the old stderr, and redirect stderr to temporary file
|
||||
try:
|
||||
self.old_stderr_fileno = os.dup(sys.stderr.fileno())
|
||||
os.dup2(self.tmpfile.fileno(), sys.stderr.fileno())
|
||||
except UnsupportedOperation:
|
||||
self.old_stderr_fileno = None
|
||||
return
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
# Restore old stderr
|
||||
if self.old_stderr_fileno is not None:
|
||||
os.dup2(self.old_stderr_fileno, sys.stderr.fileno())
|
||||
|
||||
# Get data from tmpfile (in with block to ensure it is closed)
|
||||
with self.tmpfile as tmpfile:
|
||||
tmpfile.seek(0) # Cursor will be at end, so move back to beginning
|
||||
leptonica_output = tmpfile.read().decode(errors='replace')
|
||||
|
||||
# If there are Python errors, let them bubble up
|
||||
if exc_type:
|
||||
logger.warning(leptonica_output)
|
||||
return False
|
||||
|
||||
# If there are Leptonica errors, wrap them in Python excpetions
|
||||
if 'Error' in leptonica_output:
|
||||
if 'image file not found' in leptonica_output:
|
||||
raise FileNotFoundError()
|
||||
if 'pixWrite: stream not opened' in leptonica_output:
|
||||
raise LeptonicaIOError()
|
||||
raise LeptonicaError(leptonica_output)
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class LeptonicaError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class LeptonicaIOError(LeptonicaError):
|
||||
pass
|
||||
|
||||
|
||||
class RemoveColormap(Enum):
|
||||
to_binary = 0
|
||||
to_grayscale = 1
|
||||
to_full_color = 2
|
||||
based_on_src = 3
|
||||
|
||||
|
||||
class Pix:
|
||||
"""Wrapper around leptonica's PIX object.
|
||||
|
||||
Leptonica uses referencing counting on PIX objects. Also, many Leptonica
|
||||
functions return the original object with an increased reference count
|
||||
if the operation had no effect (for example, image skew was found to be 0).
|
||||
This has complications for memory management in Python. Whenever Leptonica
|
||||
returns a PIX object (new or old), we wrap it in this class, which
|
||||
registers it with the FFI garbage collector. pixDestroy() decrements the
|
||||
reference count and only destroys when the last reference is removed.
|
||||
|
||||
Leptonica's reference counting is not threadsafe. This class can be used
|
||||
in a threadsafe manner if a Python threading.Lock protects the data.
|
||||
|
||||
This class treats Pix objects as immutable. All methods return new
|
||||
modified objects. This allows convenient chaining:
|
||||
|
||||
>>> Pix.read('filename.jpg').scale((0.5, 0.5)).deskew().show()
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, pix):
|
||||
self._pix = ffi.gc(pix, Pix._pix_destroy)
|
||||
|
||||
def __repr__(self):
|
||||
if self._pix:
|
||||
s = "<leptonica.Pix image size={0}x{1} depth={2} at 0x{3:x}>"
|
||||
return s.format(self._pix.w, self._pix.h, self._pix.d,
|
||||
int(ffi.cast("intptr_t", self._pix)))
|
||||
else:
|
||||
return "<leptonica.Pix image NULL>"
|
||||
|
||||
def _repr_png_(self):
|
||||
"""iPython display hook
|
||||
|
||||
returns png version of image
|
||||
"""
|
||||
|
||||
data = ffi.new('l_uint8 **')
|
||||
size = ffi.new('size_t *')
|
||||
|
||||
err = lept.pixWriteMemPng(data, size, self._pix, 0)
|
||||
if err != 0:
|
||||
raise LeptonicaIOError("pixWriteMemPng")
|
||||
|
||||
char_data = ffi.cast('char *', data[0])
|
||||
return ffi.buffer(char_data, size[0])[:]
|
||||
|
||||
def __getstate__(self):
|
||||
data = ffi.new('l_uint32 **')
|
||||
size = ffi.new('size_t *')
|
||||
|
||||
err = lept.pixSerializeToMemory(self._pix, data, size)
|
||||
if err != 0:
|
||||
raise LeptonicaIOError("pixSerializeToMemory")
|
||||
|
||||
char_data = ffi.cast('char *', data[0])
|
||||
|
||||
# Copy from C bytes to python bytes()
|
||||
data_bytes = ffi.buffer(char_data, size[0])[:]
|
||||
|
||||
# Can now free C bytes
|
||||
lept.lept_free(char_data)
|
||||
return dict(data=data_bytes)
|
||||
|
||||
def __setstate__(self, state):
|
||||
cdata_bytes = ffi.new('char[]', state['data'])
|
||||
cdata_uint32 = ffi.cast('l_uint32 *', cdata_bytes)
|
||||
|
||||
pix = lept.pixDeserializeFromMemory(
|
||||
cdata_uint32, len(state['data']))
|
||||
Pix.__init__(self, pix)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.__getstate__() == other.__getstate__()
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
return self._pix.w
|
||||
|
||||
@property
|
||||
def height(self):
|
||||
return self._pix.h
|
||||
|
||||
@property
|
||||
def depth(self):
|
||||
return self._pix.d
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
return (self._pix.w, self._pix.h)
|
||||
|
||||
@property
|
||||
def info(self):
|
||||
return {'dpi': (self._pix.xres, self._pix.yres)}
|
||||
|
||||
@property
|
||||
def mode(self):
|
||||
"Return mode like PIL.Image"
|
||||
if self.depth == 1:
|
||||
return '1'
|
||||
elif self.depth >= 16:
|
||||
return 'RGB'
|
||||
elif not self._pix.colormap:
|
||||
return 'L'
|
||||
else:
|
||||
return 'P'
|
||||
|
||||
@classmethod
|
||||
def read(cls, filename):
|
||||
"""Load an image file into a PIX object.
|
||||
|
||||
Leptonica can load TIFF, PNM (PBM, PGM, PPM), PNG, and JPEG. If
|
||||
loading fails then the object will wrap a C null pointer.
|
||||
"""
|
||||
with LeptonicaErrorTrap():
|
||||
return cls(lept.pixRead(
|
||||
filename.encode(sys.getfilesystemencoding())))
|
||||
|
||||
def write_implied_format(
|
||||
self, filename, jpeg_quality=0, jpeg_progressive=0):
|
||||
"""Write pix to the filename, with the extension indicating format.
|
||||
|
||||
jpeg_quality -- quality (iff JPEG; 1 - 100, 0 for default)
|
||||
jpeg_progressive -- (iff JPEG; 0 for baseline seq., 1 for progressive)
|
||||
"""
|
||||
with LeptonicaErrorTrap():
|
||||
lept.pixWriteImpliedFormat(
|
||||
filename.encode(sys.getfilesystemencoding()),
|
||||
self._pix, jpeg_quality, jpeg_progressive)
|
||||
|
||||
def topil(self):
|
||||
"Returns a PIL.Image version of this Pix"
|
||||
from PIL import Image
|
||||
|
||||
# Leptonica manages data in words, so it implicitly does an endian
|
||||
# swap. Tell Pillow about this when it reads the data.
|
||||
pix = self
|
||||
if sys.byteorder == 'little':
|
||||
if self.mode == 'RGB':
|
||||
raw_mode = 'XBGR'
|
||||
elif self.mode == 'RGBA':
|
||||
raw_mode = 'ABGR'
|
||||
elif self.mode == '1':
|
||||
raw_mode = '1;I'
|
||||
pix = Pix(lept.pixEndianByteSwapNew(pix._pix))
|
||||
else:
|
||||
raw_mode = self.mode
|
||||
pix = Pix(lept.pixEndianByteSwapNew(pix._pix))
|
||||
else:
|
||||
raw_mode = self.mode # no endian swap needed
|
||||
|
||||
size = (pix._pix.w, pix._pix.h)
|
||||
bytecount = pix._pix.wpl * 4 * pix._pix.h
|
||||
buf = ffi.buffer(pix._pix.data, bytecount)
|
||||
stride = pix._pix.wpl * 4
|
||||
|
||||
im = Image.frombytes(self.mode, size, buf, 'raw', raw_mode, stride)
|
||||
|
||||
return im
|
||||
|
||||
def show(self):
|
||||
return self.topil().show()
|
||||
|
||||
def deskew(self, reduction_factor=0):
|
||||
"""Returns the deskewed pix object.
|
||||
|
||||
A clone of the original is returned when the algorithm cannot find a
|
||||
skew angle with sufficient confidence.
|
||||
|
||||
reduction_factor -- amount to downsample (0 for default) when searching
|
||||
for skew angle
|
||||
"""
|
||||
with LeptonicaErrorTrap():
|
||||
return Pix(lept.pixDeskew(self._pix, reduction_factor))
|
||||
|
||||
def scale(self, scale_xy):
|
||||
"Returns the pix object rescaled according to the proportions given."
|
||||
with LeptonicaErrorTrap():
|
||||
return Pix(lept.pixScale(self._pix, scale_xy[0], scale_xy[1]))
|
||||
|
||||
def rotate180(self):
|
||||
with LeptonicaErrorTrap():
|
||||
return Pix(lept.pixRotate180(ffi.NULL, self._pix))
|
||||
|
||||
def rotate_orth(self, quads):
|
||||
"Orthographic rotation, quads: 0-3, number of clockwise rotations"
|
||||
with LeptonicaErrorTrap():
|
||||
return Pix(lept.pixRotateOrth(self._pix, quads))
|
||||
|
||||
def find_skew(self):
|
||||
"""Returns a tuple (deskew angle in degrees, confidence value).
|
||||
|
||||
Returns (None, None) if no angle is available.
|
||||
"""
|
||||
with LeptonicaErrorTrap():
|
||||
angle = ffi.new('float *', 0.0)
|
||||
confidence = ffi.new('float *', 0.0)
|
||||
result = lept.pixFindSkew(self._pix, angle, confidence)
|
||||
if result == 0:
|
||||
return (angle[0], confidence[0])
|
||||
else:
|
||||
return (None, None)
|
||||
|
||||
def convert_rgb_to_luminance(self):
|
||||
with LeptonicaErrorTrap():
|
||||
gray_pix = lept.pixConvertRGBToLuminance(self._pix)
|
||||
if gray_pix:
|
||||
return Pix(gray_pix)
|
||||
return None
|
||||
|
||||
def remove_colormap(self, removal_type):
|
||||
"""Remove a palette
|
||||
|
||||
removal_type - RemovalColormap()
|
||||
"""
|
||||
|
||||
with LeptonicaErrorTrap():
|
||||
return Pix(lept.pixRemoveColormap(self._pix, removal_type))
|
||||
|
||||
def otsu_adaptive_threshold(
|
||||
self, tile_size=(300, 300), kernel_size=(4, 4), scorefract=0.1):
|
||||
with LeptonicaErrorTrap():
|
||||
sx, sy = tile_size
|
||||
smoothx, smoothy = kernel_size
|
||||
p_pix = ffi.new('PIX **')
|
||||
|
||||
result = lept.pixOtsuAdaptiveThreshold(
|
||||
self._pix,
|
||||
sx, sy,
|
||||
smoothx, smoothy,
|
||||
scorefract,
|
||||
ffi.NULL,
|
||||
p_pix)
|
||||
if result == 0:
|
||||
return Pix(p_pix[0])
|
||||
else:
|
||||
return None
|
||||
|
||||
def otsu_threshold_on_background_norm(
|
||||
self, mask=None, tile_size=(10, 15), thresh=100, mincount=50,
|
||||
bgval=255, kernel_size=(2, 2), scorefract=0.1):
|
||||
with LeptonicaErrorTrap():
|
||||
sx, sy = tile_size
|
||||
smoothx, smoothy = kernel_size
|
||||
if mask is None:
|
||||
mask = ffi.NULL
|
||||
if isinstance(mask, Pix):
|
||||
mask = mask._pix
|
||||
|
||||
thresh_pix = lept.pixOtsuThreshOnBackgroundNorm(
|
||||
self._pix,
|
||||
mask,
|
||||
sx, sy,
|
||||
thresh, mincount, bgval,
|
||||
smoothx, smoothy,
|
||||
scorefract,
|
||||
ffi.NULL
|
||||
)
|
||||
if thresh_pix == ffi.NULL:
|
||||
return None
|
||||
return Pix(thresh_pix)
|
||||
|
||||
def crop_to_foreground(
|
||||
self, threshold=128, mindist=70, erasedist=30, pagenum=0,
|
||||
showmorph=0, display=0, pdfdir=ffi.NULL):
|
||||
with LeptonicaErrorTrap():
|
||||
cropbox = Box(lept.pixFindPageForeground(
|
||||
self._pix,
|
||||
threshold,
|
||||
mindist,
|
||||
erasedist,
|
||||
pagenum,
|
||||
showmorph,
|
||||
display,
|
||||
pdfdir))
|
||||
|
||||
print(repr(cropbox))
|
||||
|
||||
cropped_pix = lept.pixClipRectangle(
|
||||
self._pix,
|
||||
cropbox._box,
|
||||
ffi.NULL)
|
||||
|
||||
return Pix(cropped_pix)
|
||||
|
||||
def clean_background_to_white(
|
||||
self, mask=None, grayscale=None, gamma=1.0, black=0, white=255):
|
||||
with LeptonicaErrorTrap():
|
||||
return Pix(lept.pixCleanBackgroundToWhite(
|
||||
self._pix,
|
||||
mask or ffi.NULL,
|
||||
grayscale or ffi.NULL,
|
||||
gamma,
|
||||
black,
|
||||
white))
|
||||
|
||||
def gamma_trc(self, gamma=1.0, minval=0, maxval=255):
|
||||
with LeptonicaErrorTrap():
|
||||
return Pix(lept.pixGammaTRC(
|
||||
ffi.NULL,
|
||||
self._pix,
|
||||
gamma,
|
||||
minval,
|
||||
maxval
|
||||
))
|
||||
|
||||
def background_norm(
|
||||
self, mask=None, grayscale=None, tile_size=(10, 15), fg_threshold=60,
|
||||
min_count=40, bg_val=200, smooth_kernel=(2, 1)):
|
||||
with LeptonicaErrorTrap():
|
||||
return Pix(lept.pixBackgroundNorm(
|
||||
self._pix,
|
||||
mask or ffi.NULL,
|
||||
grayscale or ffi.NULL,
|
||||
tile_size[0],
|
||||
tile_size[1],
|
||||
fg_threshold,
|
||||
min_count,
|
||||
bg_val,
|
||||
smooth_kernel[0],
|
||||
smooth_kernel[1]
|
||||
))
|
||||
|
||||
@staticmethod
|
||||
@lru_cache(maxsize=1)
|
||||
def make_pixel_sum_tab8():
|
||||
return lept.makePixelSumTab8()
|
||||
|
||||
@staticmethod
|
||||
def correlation_binary(pix1, pix2):
|
||||
if get_leptonica_version() < 'leptonica-1.72':
|
||||
# Older versions of Leptonica (pre-1.72) have a buggy
|
||||
# implementation of pixCorrelationBinary that overflows on larger
|
||||
# images. Ubuntu trusty has 1.70. Ubuntu PPA
|
||||
# ppa:rebuntu16/avidemux+unofficial has "leptonlib" 1.73.
|
||||
pix1_count = ffi.new('l_int32 *')
|
||||
pix2_count = ffi.new('l_int32 *')
|
||||
pixn_count = ffi.new('l_int32 *')
|
||||
tab8 = Pix.make_pixel_sum_tab8()
|
||||
|
||||
lept.pixCountPixels(pix1._pix, pix1_count, tab8)
|
||||
lept.pixCountPixels(pix2._pix, pix2_count, tab8)
|
||||
pixn = Pix(lept.pixAnd(ffi.NULL, pix1._pix, pix2._pix))
|
||||
lept.pixCountPixels(pixn._pix, pixn_count, tab8)
|
||||
|
||||
# Python converts these int32s to larger units as needed
|
||||
# to avoid overflow. Overflow happens easily here.
|
||||
correlation = (
|
||||
(pixn_count[0] * pixn_count[0]) /
|
||||
(pix1_count[0] * pix2_count[0])
|
||||
)
|
||||
return correlation
|
||||
else:
|
||||
correlation = ffi.new('float *', 0.0)
|
||||
result = lept.pixCorrelationBinary(pix1._pix, pix2._pix,
|
||||
correlation)
|
||||
if result != 0:
|
||||
raise LeptonicaError("Correlation failed")
|
||||
return correlation[0]
|
||||
|
||||
@staticmethod
|
||||
def _pix_destroy(pix):
|
||||
p_pix = ffi.new('PIX **', pix)
|
||||
lept.pixDestroy(p_pix)
|
||||
# print('pix destroy ' + repr(pix))
|
||||
|
||||
|
||||
class Box:
|
||||
"""Wrapper around Leptonica's BOX objects.
|
||||
|
||||
See class Pix for notes about reference counting.
|
||||
"""
|
||||
|
||||
def __init__(self, box):
|
||||
self._box = ffi.gc(box, Box._box_destroy)
|
||||
|
||||
def __repr__(self):
|
||||
if self._box:
|
||||
return '<leptonica.Box x={0} y={1} w={2} h={3}>'.format(
|
||||
self.x, self.y, self.w, self.h)
|
||||
return '<leptonica.Box NULL>'
|
||||
|
||||
@property
|
||||
def x(self):
|
||||
return self._box.x
|
||||
|
||||
@property
|
||||
def y(self):
|
||||
return self._box.y
|
||||
|
||||
@property
|
||||
def w(self):
|
||||
return self._box.w
|
||||
|
||||
@property
|
||||
def h(self):
|
||||
return self._box.h
|
||||
|
||||
@staticmethod
|
||||
def _box_destroy(box):
|
||||
p_box = ffi.new('BOX **', box)
|
||||
lept.boxDestroy(p_box)
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def get_leptonica_version():
|
||||
"""Get Leptonica version string.
|
||||
|
||||
Caveat: Leptonica expects the caller to free this memory. We don't,
|
||||
since that would involve binding to libc to access libc.free(),
|
||||
a pointless effort to reclaim 100 bytes of memory.
|
||||
"""
|
||||
return ffi.string(lept.getLeptonicaVersion()).decode()
|
||||
|
||||
|
||||
def deskew(infile, outfile, dpi):
|
||||
try:
|
||||
pix_source = Pix.read(infile)
|
||||
except LeptonicaIOError:
|
||||
raise LeptonicaIOError("Failed to open file: %s" % infile)
|
||||
|
||||
if dpi < 150:
|
||||
reduction_factor = 1 # Don't downsample too much if DPI is already low
|
||||
else:
|
||||
reduction_factor = 0 # Use default
|
||||
pix_deskewed = pix_source.deskew(reduction_factor)
|
||||
|
||||
try:
|
||||
pix_deskewed.write_implied_format(outfile)
|
||||
except LeptonicaIOError:
|
||||
raise LeptonicaIOError("Failed to open destination file: %s" % outfile)
|
||||
|
||||
|
||||
def remove_background(infile, outfile, tile_size=(40, 60), gamma=1.0,
|
||||
black_threshold=70, white_threshold=190):
|
||||
try:
|
||||
pix = Pix.read(infile)
|
||||
except LeptonicaIOError:
|
||||
raise LeptonicaIOError("Failed to open file: %s" % infile)
|
||||
|
||||
pix = pix.background_norm(tile_size=tile_size).gamma_trc(
|
||||
gamma, black_threshold, white_threshold)
|
||||
|
||||
try:
|
||||
pix.write_implied_format(outfile)
|
||||
except LeptonicaIOError:
|
||||
raise LeptonicaIOError("Failed to open destination file: %s" % outfile)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Python wrapper to access Leptonica")
|
||||
|
||||
subparsers = parser.add_subparsers(title='commands',
|
||||
description='supported operations')
|
||||
|
||||
parser_deskew = subparsers.add_parser('deskew')
|
||||
parser_deskew.add_argument('-r', '--dpi', dest='dpi', action='store',
|
||||
type=int, default=300, help='input resolution')
|
||||
parser_deskew.add_argument('infile', help='image to deskew')
|
||||
parser_deskew.add_argument('outfile', help='deskewed output image')
|
||||
parser_deskew.set_defaults(func=deskew)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if get_leptonica_version() != u'leptonica-1.69':
|
||||
print("Unexpected leptonica version: %s" % getLeptonicaVersion())
|
||||
|
||||
args.func(args)
|
||||
|
||||
|
||||
def test_skew_angle():
|
||||
from PIL import Image, ImageDraw
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
im = Image.new(mode='1', size=(1000, 1000), color=1)
|
||||
|
||||
draw = ImageDraw.Draw(im)
|
||||
for n in range(20):
|
||||
draw.line([(50, 25 + 50*n), (950, 25 + 50*n)], width=1)
|
||||
del draw
|
||||
|
||||
test_angles = [0.1 * ang for ang in range(1, 10)] + \
|
||||
[float(ang) for ang in range(1, 7)]
|
||||
test_angles += [-ang for ang in test_angles]
|
||||
test_angles = sorted(test_angles)
|
||||
|
||||
for rotate_angle in test_angles:
|
||||
rotated_im = im.rotate(rotate_angle)
|
||||
with NamedTemporaryFile(prefix='lept-skew', suffix='.png', delete=True) as tmpfile:
|
||||
rotated_im.save(tmpfile)
|
||||
pix = pixRead(tmpfile.name)
|
||||
angle, confidence = pixFindSkew(pix)
|
||||
print('{0} {1} {2}'.format(rotate_angle, angle, confidence), file=sys.stderr)
|
||||
|
||||
|
||||
@@ -1,192 +0,0 @@
|
||||
from cffi import FFI
|
||||
|
||||
ffi = FFI()
|
||||
ffi.set_source("ocrmypdf.lib._leptonica", None)
|
||||
ffi.cdef("""
|
||||
typedef signed char l_int8;
|
||||
typedef unsigned char l_uint8;
|
||||
typedef short l_int16;
|
||||
typedef unsigned short l_uint16;
|
||||
typedef int l_int32;
|
||||
typedef unsigned int l_uint32;
|
||||
typedef float l_float32;
|
||||
typedef double l_float64;
|
||||
typedef long long l_int64;
|
||||
typedef unsigned long long l_uint64;
|
||||
|
||||
struct Pix
|
||||
{
|
||||
l_uint32 w; /* width in pixels */
|
||||
l_uint32 h; /* height in pixels */
|
||||
l_uint32 d; /* depth in bits (bpp) */
|
||||
l_uint32 spp; /* number of samples per pixel */
|
||||
l_uint32 wpl; /* 32-bit words/line */
|
||||
l_uint32 refcount; /* reference count (1 if no clones) */
|
||||
l_int32 xres; /* image res (ppi) in x direction */
|
||||
/* (use 0 if unknown) */
|
||||
l_int32 yres; /* image res (ppi) in y direction */
|
||||
/* (use 0 if unknown) */
|
||||
l_int32 informat; /* input file format, IFF_* */
|
||||
l_int32 special; /* special instructions for I/O, etc */
|
||||
char *text; /* text string associated with pix */
|
||||
struct PixColormap *colormap; /* colormap (may be null) */
|
||||
l_uint32 *data; /* the image data */
|
||||
};
|
||||
typedef struct Pix PIX;
|
||||
|
||||
struct PixColormap
|
||||
{
|
||||
void *array; /* colormap table (array of RGBA_QUAD) */
|
||||
l_int32 depth; /* of pix (1, 2, 4 or 8 bpp) */
|
||||
l_int32 nalloc; /* number of color entries allocated */
|
||||
l_int32 n; /* number of color entries used */
|
||||
};
|
||||
typedef struct PixColormap PIXCMAP;
|
||||
|
||||
struct Box
|
||||
{
|
||||
l_int32 x;
|
||||
l_int32 y;
|
||||
l_int32 w;
|
||||
l_int32 h;
|
||||
l_uint32 refcount; /* reference count (1 if no clones) */
|
||||
|
||||
};
|
||||
typedef struct Box BOX;
|
||||
|
||||
""")
|
||||
|
||||
ffi.cdef("""
|
||||
PIX * pixRead ( const char *filename );
|
||||
PIX * pixScale ( PIX *pixs, l_float32 scalex, l_float32 scaley );
|
||||
l_int32 pixFindSkew ( PIX *pixs, l_float32 *pangle, l_float32 *pconf );
|
||||
l_int32 pixWriteImpliedFormat ( const char *filename, PIX *pix, l_int32 quality, l_int32 progressive );
|
||||
l_int32
|
||||
pixWriteMemPng(l_uint8 **pdata,
|
||||
size_t *psize,
|
||||
PIX *pix,
|
||||
l_float32 gamma);
|
||||
|
||||
void pixDestroy ( PIX **ppix );
|
||||
|
||||
PIX *
|
||||
pixEndianByteSwapNew(PIX *pixs);
|
||||
|
||||
PIX * pixDeskew ( PIX *pixs, l_int32 redsearch );
|
||||
char * getLeptonicaVersion ( );
|
||||
l_int32 pixCorrelationBinary(PIX *pix1, PIX *pix2, l_float32 *pval);
|
||||
PIX *pixRotate180(PIX *pixd, PIX *pixs);
|
||||
PIX *
|
||||
pixRotateOrth(PIX *pixs,
|
||||
l_int32 quads);
|
||||
|
||||
l_int32 pixCountPixels ( PIX *pix, l_int32 *pcount, l_int32 *tab8 );
|
||||
PIX * pixAnd ( PIX *pixd, PIX *pixs1, PIX *pixs2 );
|
||||
l_int32 * makePixelSumTab8 ( void );
|
||||
|
||||
PIX * pixDeserializeFromMemory ( const l_uint32 *data, size_t nbytes );
|
||||
l_int32 pixSerializeToMemory ( PIX *pixs, l_uint32 **pdata, size_t *pnbytes );
|
||||
|
||||
PIX * pixConvertRGBToLuminance(PIX *pixs);
|
||||
|
||||
PIX * pixRemoveColormap(PIX *pixs, l_int32 type);
|
||||
|
||||
l_int32
|
||||
pixOtsuAdaptiveThreshold(PIX *pixs,
|
||||
l_int32 sx,
|
||||
l_int32 sy,
|
||||
l_int32 smoothx,
|
||||
l_int32 smoothy,
|
||||
l_float32 scorefract,
|
||||
PIX **ppixth,
|
||||
PIX **ppixd);
|
||||
|
||||
PIX *
|
||||
pixOtsuThreshOnBackgroundNorm(PIX *pixs,
|
||||
PIX *pixim,
|
||||
l_int32 sx,
|
||||
l_int32 sy,
|
||||
l_int32 thresh,
|
||||
l_int32 mincount,
|
||||
l_int32 bgval,
|
||||
l_int32 smoothx,
|
||||
l_int32 smoothy,
|
||||
l_float32 scorefract,
|
||||
l_int32 *pthresh);
|
||||
|
||||
PIX *
|
||||
pixCleanBackgroundToWhite(PIX *pixs,
|
||||
PIX *pixim,
|
||||
PIX *pixg,
|
||||
l_float32 gamma,
|
||||
l_int32 blackval,
|
||||
l_int32 whiteval);
|
||||
|
||||
BOX *
|
||||
pixFindPageForeground(PIX *pixs,
|
||||
l_int32 threshold,
|
||||
l_int32 mindist,
|
||||
l_int32 erasedist,
|
||||
l_int32 pagenum,
|
||||
l_int32 showmorph,
|
||||
l_int32 display,
|
||||
const char *pdfdir);
|
||||
|
||||
PIX *
|
||||
pixClipRectangle(PIX *pixs,
|
||||
BOX *box,
|
||||
BOX **pboxc);
|
||||
|
||||
PIX *
|
||||
pixBackgroundNorm(PIX *pixs,
|
||||
PIX *pixim,
|
||||
PIX *pixg,
|
||||
l_int32 sx,
|
||||
l_int32 sy,
|
||||
l_int32 thresh,
|
||||
l_int32 mincount,
|
||||
l_int32 bgval,
|
||||
l_int32 smoothx,
|
||||
l_int32 smoothy);
|
||||
|
||||
PIX *
|
||||
pixGammaTRC(PIX *pixd,
|
||||
PIX *pixs,
|
||||
l_float32 gamma,
|
||||
l_int32 minval,
|
||||
l_int32 maxval);
|
||||
|
||||
|
||||
l_int32
|
||||
pixNumSignificantGrayColors(PIX *pixs,
|
||||
l_int32 darkthresh,
|
||||
l_int32 lightthresh,
|
||||
l_float32 minfract,
|
||||
l_int32 factor,
|
||||
l_int32 *pncolors);
|
||||
|
||||
l_int32
|
||||
pixColorFraction(PIX *pixs,
|
||||
l_int32 darkthresh,
|
||||
l_int32 lightthresh,
|
||||
l_int32 diffthresh,
|
||||
l_int32 factor,
|
||||
l_float32 *ppixfract,
|
||||
l_float32 *pcolorfract);
|
||||
|
||||
PIX *
|
||||
pixColorMagnitude(PIX *pixs,
|
||||
l_int32 rwhite,
|
||||
l_int32 gwhite,
|
||||
l_int32 bwhite,
|
||||
l_int32 type);
|
||||
|
||||
void
|
||||
boxDestroy(BOX **pbox);
|
||||
|
||||
void lept_free(void *ptr);
|
||||
""")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
ffi.compile()
|
||||
@@ -1,7 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# © 2015-16 James R. Barlow: github.com/jbarlow83
|
||||
|
||||
# This file is now an alias for __main__
|
||||
# Consider removing in future releases
|
||||
|
||||
from ocrmypdf.__main__ import *
|
||||
@@ -1,415 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# © 2015 James R. Barlow: github.com/jbarlow83
|
||||
|
||||
from subprocess import Popen, PIPE
|
||||
from decimal import Decimal, getcontext
|
||||
from math import hypot
|
||||
import re
|
||||
import sys
|
||||
import PyPDF2 as pypdf
|
||||
from collections import namedtuple
|
||||
|
||||
try:
|
||||
from math import isclose
|
||||
except ImportError:
|
||||
def isclose(a, b, rel_tol=1e-9):
|
||||
"Python 3.4 does not have math.isclose()"
|
||||
diff = abs(b - a)
|
||||
return diff <= abs(rel_tol * b) or diff <= abs(rel_tol * a)
|
||||
|
||||
matrix_mult = pypdf.pdf.utils.matrixMultiply
|
||||
|
||||
FRIENDLY_COLORSPACE = {
|
||||
'/DeviceGray': 'gray',
|
||||
'/CalGray': 'gray',
|
||||
'/DeviceRGB': 'rgb',
|
||||
'/CalRGB': 'rgb',
|
||||
'/DeviceCMYK': 'cmyk',
|
||||
'/Lab': 'lab',
|
||||
'/ICCBased': 'icc',
|
||||
'/Indexed': 'index',
|
||||
'/Separation': 'sep',
|
||||
'/DeviceN': 'devn',
|
||||
'/Pattern': '-',
|
||||
'/G': 'gray', # Abbreviations permitted in inline images
|
||||
'/RGB': 'rgb',
|
||||
'/CMYK': 'cmyk',
|
||||
'/I': 'index',
|
||||
}
|
||||
|
||||
FRIENDLY_ENCODING = {
|
||||
'/CCITTFaxDecode': 'ccitt',
|
||||
'/DCTDecode': 'jpeg',
|
||||
'/JPXDecode': 'jpx',
|
||||
'/JBIG2Decode': 'jbig2',
|
||||
'/CCF': 'ccitt', # Abbreviations permitted in inline images
|
||||
'/DCT': 'jpeg',
|
||||
'/AHx': 'asciihex',
|
||||
'/A85': 'ascii85',
|
||||
'/LZW': 'lzw',
|
||||
'/Fl': 'flate',
|
||||
'/RL': 'runlength'
|
||||
}
|
||||
|
||||
FRIENDLY_COMP = {
|
||||
'gray': 1,
|
||||
'rgb': 3,
|
||||
'cmyk': 4,
|
||||
'lab': 3,
|
||||
'index': 1
|
||||
}
|
||||
|
||||
|
||||
UNIT_SQUARE = (1.0, 0.0, 0.0, 1.0, 0.0, 0.0)
|
||||
|
||||
|
||||
def _matrix_from_shorthand(shorthand):
|
||||
"""Convert from PDF matrix shorthand to full matrix
|
||||
|
||||
PDF 1.7 spec defines a shorthand for describing the entries of a matrix
|
||||
since the last column is always (0, 0, 1).
|
||||
"""
|
||||
|
||||
a, b, c, d, e, f = map(float, shorthand)
|
||||
return ((a, b, 0),
|
||||
(c, d, 0),
|
||||
(e, f, 1))
|
||||
|
||||
|
||||
def _shorthand_from_matrix(matrix):
|
||||
"""Convert from transformation matrix to PDF shorthand."""
|
||||
a, b = matrix[0][0], matrix[0][1]
|
||||
c, d = matrix[1][0], matrix[1][1]
|
||||
e, f = matrix[2][0], matrix[2][1]
|
||||
return tuple(map(float, (a, b, c, d, e, f)))
|
||||
|
||||
|
||||
def _is_unit_square(shorthand):
|
||||
values = map(float, shorthand)
|
||||
pairwise = zip(values, UNIT_SQUARE)
|
||||
return all([isclose(a, b, rel_tol=1e-3) for a, b in pairwise])
|
||||
|
||||
RasterSettings = namedtuple('RasterSettings',
|
||||
['name', 'shorthand', 'stack_depth'])
|
||||
|
||||
InlineSettings = namedtuple('InlineSettings',
|
||||
['settings', 'shorthand', 'stack_depth'])
|
||||
|
||||
ContentsInfo = namedtuple('ContentsInfo', ['raster_settings', 'inline_images'])
|
||||
|
||||
|
||||
def _interpret_contents(contentstream):
|
||||
"""Interpret the PDF content stream
|
||||
|
||||
The stack represents the state of the PDF graphics stack. We are only
|
||||
interested in the current transformation matrix (CTM) so we only track
|
||||
this object; a full implementation would need to track many other items.
|
||||
|
||||
The CTM is initialized to the mapping from user space to device space.
|
||||
PDF units are 1/72". In a PDF viewer or printer this matrix is initialized
|
||||
to the transformation to device space. For example if set to
|
||||
(1/72, 0, 0, 1/72, 0, 0) then all units would be calculated in inches.
|
||||
|
||||
Images are always considered to be (0, 0) -> (1, 1). Before drawing an
|
||||
image there should be a 'cm' that sets up an image coordinate system
|
||||
where drawing from (0, 0) -> (1, 1) will draw on the desired area of the
|
||||
page.
|
||||
|
||||
PDF units suit our needs so we initialize ctm to the identity matrix.
|
||||
|
||||
PyPDF2 replaces inline images with a fake "INLINE IMAGE" operator.
|
||||
|
||||
"""
|
||||
|
||||
operations = contentstream.operations
|
||||
stack = []
|
||||
ctm = _matrix_from_shorthand(UNIT_SQUARE)
|
||||
image_raster_settings = []
|
||||
inline_images = []
|
||||
|
||||
for op in operations:
|
||||
operands, command = op
|
||||
if command == b'q':
|
||||
stack.append(ctm)
|
||||
if len(stack) > 32:
|
||||
raise RuntimeError("PDF graphics stack overflow")
|
||||
elif command == b'Q':
|
||||
ctm = stack.pop()
|
||||
elif command == b'cm':
|
||||
ctm = matrix_mult(
|
||||
_matrix_from_shorthand(operands), ctm)
|
||||
elif command == b'Do':
|
||||
image_name = operands[0]
|
||||
raster = RasterSettings(
|
||||
name=image_name, shorthand=_shorthand_from_matrix(ctm),
|
||||
stack_depth=len(stack))
|
||||
image_raster_settings.append(raster)
|
||||
elif command == b'INLINE IMAGE':
|
||||
settings = operands['settings']
|
||||
inline = InlineSettings(
|
||||
settings=settings, shorthand=_shorthand_from_matrix(ctm),
|
||||
stack_depth=len(stack))
|
||||
inline_images.append(inline)
|
||||
|
||||
return ContentsInfo(
|
||||
raster_settings=image_raster_settings,
|
||||
inline_images=inline_images)
|
||||
|
||||
|
||||
def _get_dpi(ctm_shorthand, image_size):
|
||||
"""Given the transformation matrix and image size, find the image DPI.
|
||||
|
||||
PDFs do not include image resolution information within image data.
|
||||
Instead, the PDF page content stream describes the location where the
|
||||
image will be rasterized, and the effective resolution is the ratio of the
|
||||
pixel size to raster target size.
|
||||
|
||||
Normally a scanned PDF has the paper size set appropriately but this is
|
||||
not guaranteed. The most common case is a cropped image will change the
|
||||
page size (/CropBox) without altering the page content stream. That means
|
||||
it is not sufficient to assume that the image fills the page, even though
|
||||
that is the most common case.
|
||||
|
||||
A PDF image may be scaled (always), cropped, translated, rotated in place
|
||||
to an arbitrary angle (rarely) and skewed. Only equal area mappings can
|
||||
be expressed, that is, it is not necessary to consider distortions where
|
||||
the effective DPI varies with position.
|
||||
|
||||
To determine the image scale, transform an offset axis vector v0 (0, 0),
|
||||
width-axis vector v0 (1, 0), height-axis vector vh (0, 1) with the matrix,
|
||||
which gives the dimensions of the image in PDF units. From there we can
|
||||
compare to actual image dimensions. PDF uses
|
||||
row vector * matrix_tranposed unlike the traditional
|
||||
matrix * column vector.
|
||||
|
||||
The offset, width and height vectors can be combined in a matrix and
|
||||
multiplied by the transform matrix. Then we want to calculated
|
||||
magnitude(width_vector - offset_vector)
|
||||
and
|
||||
magnitude(height_vector - offset_vector)
|
||||
|
||||
When the above is worked out algebraically, the effect of translation
|
||||
cancels out, and the vector magnitudes become functions of the nonzero
|
||||
transformation matrix indices. The results of the derivation are used
|
||||
in this code.
|
||||
|
||||
pdfimages -list does calculate the DPI in some way that is not completely
|
||||
naive, but it does not get the DPI of rotated images right, so cannot be
|
||||
used anymore to validate this. Photoshop works, or using Acrobat to
|
||||
rotate the image back to normal.
|
||||
|
||||
It does not matter if the image is partially cropped, or even out of the
|
||||
/MediaBox.
|
||||
|
||||
"""
|
||||
|
||||
a, b, c, d, _, _ = ctm_shorthand
|
||||
|
||||
# Calculate the width and height of the image in PDF units
|
||||
image_drawn_width = hypot(a, b)
|
||||
image_drawn_height = hypot(c, d)
|
||||
|
||||
# The scale of the image is pixels per PDF unit (1/72")
|
||||
scale_w = image_size[0] / image_drawn_width
|
||||
scale_h = image_size[1] / image_drawn_height
|
||||
|
||||
# DPI = scale * 72
|
||||
dpi_w = scale_w * 72.0
|
||||
dpi_h = scale_h * 72.0
|
||||
|
||||
return (dpi_w, dpi_h)
|
||||
|
||||
|
||||
def _find_page_inline_images(page, pageinfo, contentsinfo):
|
||||
"Find inline images on the page"
|
||||
|
||||
for n, inline in enumerate(contentsinfo.inline_images):
|
||||
image = {}
|
||||
image['name'] = str('inline-%02d' % n)
|
||||
image['width'] = inline.settings['/W']
|
||||
image['height'] = inline.settings['/H']
|
||||
if '/BPC' in inline.settings:
|
||||
image['bpc'] = inline.settings['/BPC']
|
||||
else:
|
||||
image['bpc'] = 8
|
||||
image['color'] = FRIENDLY_COLORSPACE.get(inline.settings['/CS'], '-')
|
||||
image['comp'] = FRIENDLY_COMP.get(image['color'], '?')
|
||||
if '/F' in inline.settings:
|
||||
filter_ = inline.settings['/F']
|
||||
if isinstance(filter_, pypdf.generic.ArrayObject):
|
||||
filter_ = filter_[0]
|
||||
image['enc'] = FRIENDLY_ENCODING.get(filter_, 'image')
|
||||
else:
|
||||
image['enc'] = 'image'
|
||||
|
||||
dpi_w, dpi_h = _get_dpi(
|
||||
inline.shorthand, (image['width'], image['height']))
|
||||
image['dpi_w'], image['dpi_h'] = Decimal(dpi_w), Decimal(dpi_h)
|
||||
yield image
|
||||
|
||||
|
||||
def _find_page_regular_images(page, pageinfo, contentsinfo):
|
||||
"Find images stored in XObject resources"
|
||||
|
||||
try:
|
||||
page['/Resources']['/XObject']
|
||||
except KeyError:
|
||||
return
|
||||
for xobj in page['/Resources']['/XObject']:
|
||||
# PyPDF2 returns the keys as an iterator
|
||||
pdfimage = page['/Resources']['/XObject'][xobj]
|
||||
if pdfimage['/Subtype'] != '/Image':
|
||||
continue
|
||||
image = {}
|
||||
image['name'] = str(xobj)
|
||||
image['width'] = pdfimage['/Width']
|
||||
image['height'] = pdfimage['/Height']
|
||||
if '/BitsPerComponent' in pdfimage:
|
||||
image['bpc'] = pdfimage['/BitsPerComponent']
|
||||
else:
|
||||
image['bpc'] = 8
|
||||
|
||||
# Fixme: this is incorrectly treats explicit masks as stencil masks,
|
||||
# but good enough for now. Explicit masks have /ImageMask true but are
|
||||
# never called for in content stream, instead are drawn as a /Mask on
|
||||
# other images. For our purposes finding out the details of /Mask
|
||||
# will seldom matter.
|
||||
if '/ImageMask' in pdfimage:
|
||||
image['type'] = 'stencil' if pdfimage['/ImageMask'].value \
|
||||
else 'image'
|
||||
else:
|
||||
image['type'] = 'image'
|
||||
if '/Filter' in pdfimage:
|
||||
filter_ = pdfimage['/Filter']
|
||||
if isinstance(filter_, pypdf.generic.ArrayObject):
|
||||
filter_ = filter_[0]
|
||||
image['enc'] = FRIENDLY_ENCODING.get(filter_, 'image')
|
||||
else:
|
||||
image['enc'] = 'image'
|
||||
if '/ColorSpace' in pdfimage:
|
||||
cs = pdfimage['/ColorSpace']
|
||||
if isinstance(cs, pypdf.generic.ArrayObject):
|
||||
cs = cs[0]
|
||||
image['color'] = FRIENDLY_COLORSPACE.get(cs, '-')
|
||||
else:
|
||||
image['color'] = 'jpx' if image['enc'] == 'jpx' else '?'
|
||||
|
||||
image['comp'] = FRIENDLY_COMP.get(image['color'], '?')
|
||||
|
||||
# Bit of a hack... infer grayscale if component count is uncertain
|
||||
# but encoding must be monochrome. This happens if a monochrome image
|
||||
# has an ICC profile attached. Better solution would be to examine
|
||||
# the ICC profile.
|
||||
if image['comp'] == '?' and image['enc'] in ('ccitt', 'jbig2'):
|
||||
image['comp'] = FRIENDLY_COMP['gray']
|
||||
|
||||
image['dpi_w'] = image['dpi_h'] = 0
|
||||
|
||||
for raster in contentsinfo.raster_settings:
|
||||
# Loop in case the same image is display multiple times on a page
|
||||
if raster.name != image['name']:
|
||||
continue
|
||||
|
||||
if raster.stack_depth == 0 and _is_unit_square(raster.shorthand):
|
||||
# At least one PDF in the wild (and test suite) draws an image
|
||||
# when the graphics stack depth is 0, meaning that the image
|
||||
# gets drawn into a square of 1x1 PDF units (or 1/72",
|
||||
# or 0.35 mm). The equivalent DPI will be >100,000. Exclude
|
||||
# these from our DPI calculation for the page.
|
||||
continue
|
||||
|
||||
dpi_w, dpi_h = _get_dpi(
|
||||
raster.shorthand, (image['width'], image['height']))
|
||||
|
||||
# When image is used multiple times take the highest DPI it is
|
||||
# rendered at
|
||||
image['dpi_w'] = max(dpi_w, image.get('dpi_w', 0))
|
||||
image['dpi_h'] = max(dpi_h, image.get('dpi_h', 0))
|
||||
|
||||
image['dpi_w'] = Decimal(image['dpi_w'])
|
||||
image['dpi_h'] = Decimal(image['dpi_h'])
|
||||
image['dpi'] = (image['dpi_w'] * image['dpi_h']) ** Decimal(0.5)
|
||||
yield image
|
||||
|
||||
|
||||
def _find_page_images(page, pageinfo, contentsinfo):
|
||||
yield from _find_page_inline_images(page, pageinfo, contentsinfo)
|
||||
yield from _find_page_regular_images(page, pageinfo, contentsinfo)
|
||||
|
||||
|
||||
def _page_has_text(pdf, page):
|
||||
# Simple test
|
||||
text = page.extractText()
|
||||
if text.strip() != '':
|
||||
return True
|
||||
|
||||
# More nuanced test to deal with quirks of Tesseract PDF generation
|
||||
# Check if there's a Glyphless font
|
||||
try:
|
||||
font = page['/Resources']['/Font']
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
font_objects = list(font.keys())
|
||||
for font_object in font_objects:
|
||||
basefont = font[font_object]['/BaseFont']
|
||||
if basefont.endswith('GlyphLessFont'):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _pdf_get_pageinfo(infile, pageno: int):
|
||||
pageinfo = {}
|
||||
pageinfo['pageno'] = pageno
|
||||
pageinfo['images'] = []
|
||||
|
||||
pdf = pypdf.PdfFileReader(infile)
|
||||
page = pdf.pages[pageno]
|
||||
|
||||
pageinfo['has_text'] = _page_has_text(pdf, page)
|
||||
|
||||
width_pt = page.mediaBox.getWidth()
|
||||
height_pt = page.mediaBox.getHeight()
|
||||
pageinfo['width_inches'] = width_pt / Decimal(72.0)
|
||||
pageinfo['height_inches'] = height_pt / Decimal(72.0)
|
||||
|
||||
try:
|
||||
contentstream = pypdf.pdf.ContentStream(page.getContents(), pdf)
|
||||
except AttributeError as e:
|
||||
return pageinfo
|
||||
|
||||
contentsinfo = _interpret_contents(contentstream)
|
||||
pageinfo['images'] = [im for im in _find_page_images(
|
||||
page, pageinfo, contentsinfo)]
|
||||
|
||||
if pageinfo['images']:
|
||||
xres = max(image['dpi_w'] for image in pageinfo['images'])
|
||||
yres = max(image['dpi_h'] for image in pageinfo['images'])
|
||||
pageinfo['xres'], pageinfo['yres'] = xres, yres
|
||||
pageinfo['width_pixels'] = \
|
||||
int(round(xres * pageinfo['width_inches']))
|
||||
pageinfo['height_pixels'] = \
|
||||
int(round(yres * pageinfo['height_inches']))
|
||||
|
||||
return pageinfo
|
||||
|
||||
|
||||
def pdf_get_all_pageinfo(infile):
|
||||
pdf = pypdf.PdfFileReader(infile)
|
||||
getcontext().prec = 6
|
||||
return [_pdf_get_pageinfo(infile, n) for n in range(pdf.numPages)]
|
||||
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('infile')
|
||||
args = parser.parse_args()
|
||||
info = pdf_get_all_pageinfo(args.infile)
|
||||
from pprint import pprint
|
||||
pprint(info)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,160 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# © 2015 James R. Barlow: github.com/jbarlow83
|
||||
#
|
||||
# Generate a PDFA_def.ps file for Ghostscript >= 9.14
|
||||
|
||||
from __future__ import print_function, absolute_import, division
|
||||
from string import Template
|
||||
import codecs
|
||||
import pkg_resources
|
||||
import PyPDF2 as pypdf
|
||||
|
||||
ICC_PROFILE_RELPATH = 'data/sRGB.icc'
|
||||
|
||||
SRGB_ICC_PROFILE = pkg_resources.resource_filename(
|
||||
'ocrmypdf', ICC_PROFILE_RELPATH)
|
||||
|
||||
|
||||
# This is a template written in PostScript which is needed to create PDF/A
|
||||
# files, from the Ghostscript documentation. Lines beginning with % are
|
||||
# comments. Python substitution variables have a '$' prefix.
|
||||
pdfa_def_template = u"""%!
|
||||
% This is a sample prefix file for creating a PDF/A document.
|
||||
% Feel free to modify entries marked with "Customize".
|
||||
% This assumes an ICC profile to reside in the file (ISO Coated sb.icc),
|
||||
% unless the user modifies the corresponding line below.
|
||||
|
||||
% Define entries in the document Info dictionary :
|
||||
/ICCProfile ($icc_profile)
|
||||
def
|
||||
|
||||
[ /Title <$title>
|
||||
/Author <$author>
|
||||
/Subject <$subject>
|
||||
/Keywords <$keywords>
|
||||
/Creator <$creator>
|
||||
/DOCINFO pdfmark
|
||||
|
||||
% Define an ICC profile :
|
||||
|
||||
[/_objdef {icc_PDFA} /type /stream /OBJ pdfmark
|
||||
[{icc_PDFA}
|
||||
<<
|
||||
/N currentpagedevice /ProcessColorModel known {
|
||||
currentpagedevice /ProcessColorModel get dup /DeviceGray eq
|
||||
{pop 1} {
|
||||
/DeviceRGB eq
|
||||
{3}{4} ifelse
|
||||
} ifelse
|
||||
} {
|
||||
(ERROR, unable to determine ProcessColorModel) == flush
|
||||
} ifelse
|
||||
>> /PUT pdfmark
|
||||
[{icc_PDFA} ICCProfile (r) file /PUT pdfmark
|
||||
|
||||
% Define the output intent dictionary :
|
||||
|
||||
[/_objdef {OutputIntent_PDFA} /type /dict /OBJ pdfmark
|
||||
[{OutputIntent_PDFA} <<
|
||||
/Type /OutputIntent % Must be so (the standard requires).
|
||||
/S /GTS_PDFA1 % Must be so (the standard requires).
|
||||
/DestOutputProfile {icc_PDFA} % Must be so (see above).
|
||||
/OutputConditionIdentifier ($icc_identifier)
|
||||
>> /PUT pdfmark
|
||||
[{Catalog} <</OutputIntents [ {OutputIntent_PDFA} ]>> /PUT pdfmark
|
||||
"""
|
||||
|
||||
|
||||
def encode_text_string(s: str) -> str:
|
||||
'''Encode text string to hex string for use in a PDF
|
||||
|
||||
From PDF 32000-1:2008 a string object may be included in hexademical form
|
||||
if it is enclosed in angle brackets. For general Unicode the string should
|
||||
be UTF-16 (big endian) with byte order marks. Many strings including all
|
||||
ASCII strings fall could be encoded as PdfDocEncoding literals provided
|
||||
that certain Postscript sequences are escaped. But it's far simpler to
|
||||
encode everything as UTF-16.
|
||||
'''
|
||||
|
||||
# Sometimes lazy C programmers leave their NULs at the end of strings they
|
||||
# insert into PDFs
|
||||
# tests/resources/aspect.pdf is one example (created by ImageMagick)
|
||||
s = s.replace('\x00', '')
|
||||
|
||||
if s == '':
|
||||
return ''
|
||||
|
||||
utf16_bytes = s.encode('utf-16be')
|
||||
ascii_hex_bytes = codecs.encode(b'\xfe\xff' + utf16_bytes, 'hex')
|
||||
ascii_hex_str = ascii_hex_bytes.decode('ascii').lower()
|
||||
return ascii_hex_str
|
||||
|
||||
|
||||
def _get_pdfa_def(icc_profile, icc_identifier, pdfmark):
|
||||
pdfmark_utf16 = {k: encode_text_string(v) for k, v in pdfmark.items()}
|
||||
|
||||
t = Template(pdfa_def_template)
|
||||
result = t.substitute(icc_profile=icc_profile,
|
||||
icc_identifier=icc_identifier,
|
||||
title=pdfmark_utf16.get('/Title', ''),
|
||||
author=pdfmark_utf16.get('/Author', ''),
|
||||
subject=pdfmark_utf16.get('/Subject', ''),
|
||||
creator=pdfmark_utf16.get('/Creator', ''),
|
||||
keywords=pdfmark_utf16.get('/Keywords', ''))
|
||||
return result
|
||||
|
||||
|
||||
def generate_pdfa_def(target_filename, pdfmark, icc='sRGB'):
|
||||
if icc == 'sRGB':
|
||||
icc_profile = SRGB_ICC_PROFILE
|
||||
else:
|
||||
raise NotImplementedError("Only supporting sRGB")
|
||||
|
||||
ps = _get_pdfa_def(icc_profile, icc, pdfmark)
|
||||
|
||||
# We should have encoded everything to pure ASCII by this point, and
|
||||
# to be safe, only allow ASCII in PostScript
|
||||
with open(target_filename, 'w', encoding='ascii') as f:
|
||||
f.write(ps)
|
||||
|
||||
|
||||
def file_claims_pdfa(filename):
|
||||
"""Determines if the file claims to be PDF/A compliant
|
||||
|
||||
Checking if a file is a truly compliant PDF/A is a massive undertaking
|
||||
that no open source tool does properly. Some commercial tools are
|
||||
generally reliable (Acrobat).
|
||||
|
||||
This checks if the XMP metadata contains a PDF/A marker.
|
||||
"""
|
||||
|
||||
pdf = pypdf.PdfFileReader(filename)
|
||||
xmp = pdf.getXmpMetadata()
|
||||
|
||||
try:
|
||||
pdfa_nodes = xmp.getNodesInNamespace(
|
||||
aboutUri='',
|
||||
namespace='http://www.aiim.org/pdfa/ns/id/')
|
||||
except AttributeError:
|
||||
return {'pass': False, 'output': 'pdf',
|
||||
'conformance': 'No XMP metadata'}
|
||||
|
||||
pdfa_dict = {attr.localName: attr.value for attr in pdfa_nodes}
|
||||
pdfa_dict['pass'] = False
|
||||
pdfa_dict['output'] = 'pdf'
|
||||
if pdfa_dict:
|
||||
part_conformance = pdfa_dict['part'] + pdfa_dict['conformance']
|
||||
valid_part_conforms = {'1A', '1B', '2A', '2B', '2U', '3A', '3B', '3U'}
|
||||
|
||||
conformance = 'PDF/A-{}'.format(
|
||||
part_conformance)
|
||||
|
||||
if part_conformance in valid_part_conforms:
|
||||
pdfa_dict['pass'] = True
|
||||
pdfa_dict['output'] = 'pdfa'
|
||||
pdfa_dict['conformance'] = conformance
|
||||
else:
|
||||
pdfa_dict['conformance'] = 'PDF'
|
||||
|
||||
return pdfa_dict
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# © 2015 James R. Barlow: github.com/jbarlow83
|
||||
|
||||
from subprocess import CalledProcessError, check_output, STDOUT, check_call
|
||||
from functools import lru_cache
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
|
||||
from . import ExitCode, get_program
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def version():
|
||||
args_qpdf = [
|
||||
get_program('qpdf'),
|
||||
'--version'
|
||||
]
|
||||
try:
|
||||
versions = check_output(
|
||||
args_qpdf, close_fds=True, universal_newlines=True,
|
||||
stderr=STDOUT)
|
||||
except CalledProcessError:
|
||||
print("Could not find qpdf executable on system PATH.")
|
||||
sys.exit(ExitCode.missing_dependency)
|
||||
|
||||
qpdf_version = re.match(r'qpdf version (.+)', versions).group(1)
|
||||
return qpdf_version
|
||||
|
||||
|
||||
def check(input_file, log):
|
||||
args_qpdf = [
|
||||
get_program('qpdf'),
|
||||
'--check',
|
||||
input_file
|
||||
]
|
||||
|
||||
try:
|
||||
check_output(args_qpdf, stderr=STDOUT, universal_newlines=True)
|
||||
except CalledProcessError as e:
|
||||
if e.returncode == 2:
|
||||
log.error("{0}: not a valid PDF, and could not repair it.".format(
|
||||
input_file))
|
||||
log.error("Details:")
|
||||
log.error(e.output)
|
||||
elif e.returncode == 3:
|
||||
log.info("qpdf --check returned warnings:")
|
||||
log.info(e.output)
|
||||
else:
|
||||
log.warning(e.output)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def repair(input_file, output_file, log):
|
||||
args_qpdf = [
|
||||
get_program('qpdf'), input_file, output_file
|
||||
]
|
||||
try:
|
||||
check_output(args_qpdf, stderr=STDOUT, universal_newlines=True)
|
||||
except CalledProcessError as e:
|
||||
if e.returncode == 3 and e.output.find("operation succeeded"):
|
||||
log.debug('qpdf found and fixed errors: ' + e.output)
|
||||
log.debug(e.output)
|
||||
return
|
||||
|
||||
if e.returncode == 2 and e.output.find("invalid password"):
|
||||
log.error("{0}: this PDF is password-protected - password must "
|
||||
"be removed for OCR".format(input_file))
|
||||
sys.exit(ExitCode.input_file)
|
||||
elif e.returncode == 2:
|
||||
log.error("{0}: not a valid PDF, and could not repair it.".format(
|
||||
input_file))
|
||||
log.error("Details: " + e.output)
|
||||
sys.exit(ExitCode.input_file)
|
||||
else:
|
||||
log.error("{0}: unknown error".format(
|
||||
input_file))
|
||||
log.error(e.output)
|
||||
sys.exit(ExitCode.unknown)
|
||||
|
||||
|
||||
def get_npages(input_file, log):
|
||||
try:
|
||||
pages = check_output(
|
||||
[get_program('qpdf'), '--show-npages', input_file],
|
||||
universal_newlines=True, close_fds=True)
|
||||
except CalledProcessError as e:
|
||||
if e.returncode == 2 and e.output.find('No such file'):
|
||||
log.error(e.output)
|
||||
sys.exit(ExitCode.input_file)
|
||||
return int(pages)
|
||||
|
||||
|
||||
def split_pages(input_file, work_folder, npages):
|
||||
"""Split multipage PDF into individual pages.
|
||||
|
||||
Incredibly enough, this multiple process approach is about 70 times
|
||||
faster than using Ghostscript.
|
||||
"""
|
||||
for n in range(int(npages)):
|
||||
args_qpdf = [
|
||||
get_program('qpdf'), input_file,
|
||||
'--pages', input_file, '{0}'.format(n + 1), '--',
|
||||
os.path.join(work_folder, '{0:06d}.page.pdf'.format(n + 1))
|
||||
]
|
||||
check_call(args_qpdf)
|
||||
|
||||
|
||||
def merge(input_files, output_file):
|
||||
"""Merge the list of input files (all filenames) into the output file.
|
||||
|
||||
The input files may contain one or more pages.
|
||||
"""
|
||||
args_qpdf = [
|
||||
get_program('qpdf'), input_files[0], '--pages'
|
||||
] + input_files + ['--', output_file]
|
||||
check_call(args_qpdf)
|
||||
|
||||
@@ -1,256 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# © 2015 James R. Barlow: github.com/jbarlow83
|
||||
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
from functools import lru_cache
|
||||
from . import ExitCode, get_program, page_number
|
||||
from collections import namedtuple
|
||||
|
||||
from subprocess import Popen, PIPE, CalledProcessError, \
|
||||
TimeoutExpired, check_output, STDOUT, DEVNULL
|
||||
|
||||
|
||||
OrientationConfidence = namedtuple(
|
||||
'OrientationConfidence',
|
||||
('angle', 'confidence'))
|
||||
|
||||
HOCR_TEMPLATE = '''<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<title></title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name='ocr-system' content='tesseract 3.02.02' />
|
||||
<meta name='ocr-capabilities' content='ocr_page ocr_carea ocr_par ocr_line ocrx_word'/>
|
||||
</head>
|
||||
<body>
|
||||
<div class='ocr_page' id='page_1' title='image "x.tif"; bbox 0 0 {0} {1}; ppageno 0'>
|
||||
<div class='ocr_carea' id='block_1_1' title="bbox 0 1 {0} {1}">
|
||||
<p class='ocr_par' dir='ltr' id='par_1' title="bbox 0 1 {0} {1}">
|
||||
<span class='ocr_line' id='line_1' title="bbox 0 1 {0} {1}"><span class='ocrx_word' id='word_1' title="bbox 0 1 {0} {1}"> </span>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>'''
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def version():
|
||||
args_tess = [
|
||||
get_program('tesseract'),
|
||||
'--version'
|
||||
]
|
||||
try:
|
||||
versions = check_output(
|
||||
args_tess, close_fds=True, universal_newlines=True,
|
||||
stderr=STDOUT)
|
||||
except CalledProcessError:
|
||||
print("Could not find Tesseract executable on system PATH.")
|
||||
sys.exit(ExitCode.missing_dependency)
|
||||
|
||||
tesseract_version = re.match(r'tesseract\s(.+)', versions).group(1)
|
||||
return tesseract_version
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def languages():
|
||||
args_tess = [
|
||||
get_program('tesseract'),
|
||||
'--list-langs'
|
||||
]
|
||||
try:
|
||||
langs = check_output(
|
||||
args_tess, close_fds=True, universal_newlines=True,
|
||||
stderr=STDOUT)
|
||||
except CalledProcessError as e:
|
||||
print("Tesseract failed to report available languages.")
|
||||
print("Output from Tesseract:")
|
||||
print("-" * 40)
|
||||
print(e.output)
|
||||
sys.exit(ExitCode.missing_dependency)
|
||||
return set(lang.strip() for lang in langs.splitlines()[1:])
|
||||
|
||||
|
||||
def get_orientation(input_file, language: list, timeout: float, log):
|
||||
args_tesseract = [
|
||||
get_program('tesseract'),
|
||||
'-l', '+'.join(language),
|
||||
'-psm', '0',
|
||||
input_file,
|
||||
'stdout'
|
||||
]
|
||||
|
||||
try:
|
||||
stdout = check_output(
|
||||
args_tesseract, close_fds=True, stderr=STDOUT,
|
||||
universal_newlines=True, timeout=timeout)
|
||||
except TimeoutExpired:
|
||||
return OrientationConfidence(angle=0, confidence=0.0)
|
||||
except CalledProcessError as e:
|
||||
tesseract_log_output(log, e.output, input_file)
|
||||
if ('Too few characters. Skipping this page' in e.output or
|
||||
'Image too large' in e.output):
|
||||
return OrientationConfidence(0, 0)
|
||||
raise e from e
|
||||
else:
|
||||
osd = {}
|
||||
for line in stdout.splitlines():
|
||||
line = line.strip()
|
||||
parts = line.split(':', maxsplit=2)
|
||||
if len(parts) == 2:
|
||||
osd[parts[0].strip()] = parts[1].strip()
|
||||
|
||||
angle = int(osd.get('Orientation in degrees', 0))
|
||||
if 'Orientation' in osd:
|
||||
# Tesseract < 3.04.01
|
||||
# reports "Orientation in degrees" as a counterclockwise angle
|
||||
# We keep it clockwise
|
||||
assert 'Rotate' not in osd
|
||||
angle = -angle % 360
|
||||
else:
|
||||
# Tesseract == 3.04.01, hopefully also Tesseract > 3.04.01
|
||||
# reports "Orientation in degrees" as a clockwise angle
|
||||
assert 'Rotate' in osd
|
||||
|
||||
oc = OrientationConfidence(
|
||||
angle=angle,
|
||||
confidence=float(osd.get('Orientation confidence', 0)))
|
||||
return oc
|
||||
|
||||
|
||||
def tesseract_log_output(log, stdout, input_file):
|
||||
lines = stdout.splitlines()
|
||||
prefix = "{0:4d}: [tesseract] ".format(page_number(input_file))
|
||||
for line in lines:
|
||||
if line.startswith("Tesseract Open Source"):
|
||||
continue
|
||||
elif line.startswith("Warning in pixReadMem"):
|
||||
continue
|
||||
elif 'diacritics' in line:
|
||||
log.warning(prefix + "lots of diacritics - possibly poor OCR")
|
||||
elif line.startswith('OSD: Weak margin'):
|
||||
log.warning(prefix + "unsure about page orientation")
|
||||
elif 'error' in line.lower() or 'exception' in line.lower():
|
||||
log.error(prefix + line.strip())
|
||||
else:
|
||||
log.info(prefix + line.strip())
|
||||
|
||||
|
||||
def page_timedout(log, input_file):
|
||||
prefix = "{0:4d}: [tesseract] ".format(page_number(input_file))
|
||||
log.warning(prefix + " took too long to OCR - skipping")
|
||||
|
||||
|
||||
def _generate_null_hocr(output_hocr, pageinfo):
|
||||
with open(output_hocr, 'w', encoding="utf-8") as f:
|
||||
f.write(HOCR_TEMPLATE.format(
|
||||
pageinfo['width_pixels'],
|
||||
pageinfo['height_pixels']))
|
||||
|
||||
|
||||
def generate_hocr(input_file, output_hocr, language: list, tessconfig: list,
|
||||
timeout: float, pageinfo_getter, pagesegmode: int, log):
|
||||
|
||||
badxml = os.path.splitext(output_hocr)[0] + '.badxml'
|
||||
|
||||
args_tesseract = [
|
||||
get_program('tesseract'),
|
||||
'-l', '+'.join(language)
|
||||
]
|
||||
|
||||
if pagesegmode is not None:
|
||||
args_tesseract.extend(['-psm', str(pagesegmode)])
|
||||
|
||||
args_tesseract.extend([
|
||||
input_file,
|
||||
badxml,
|
||||
'hocr'
|
||||
] + tessconfig)
|
||||
try:
|
||||
stdout = check_output(
|
||||
args_tesseract, close_fds=True, stderr=STDOUT,
|
||||
universal_newlines=True, timeout=timeout)
|
||||
except TimeoutExpired:
|
||||
# Generate a HOCR file with no recognized text if tesseract times out
|
||||
# Temporary workaround to hocrTransform not being able to function if
|
||||
# it does not have a valid hOCR file.
|
||||
page_timedout(log, input_file)
|
||||
_generate_null_hocr(output_hocr, pageinfo_getter())
|
||||
except CalledProcessError as e:
|
||||
tesseract_log_output(log, e.output, input_file)
|
||||
if 'Image too large' in e.output:
|
||||
_generate_null_hocr(output_hocr, pageinfo_getter())
|
||||
return
|
||||
|
||||
raise e from e
|
||||
else:
|
||||
tesseract_log_output(log, stdout, input_file)
|
||||
if os.path.exists(badxml + '.html'):
|
||||
# Tesseract 3.02 appends suffix ".html" on its own (.badxml.html)
|
||||
shutil.move(badxml + '.html', badxml)
|
||||
elif os.path.exists(badxml + '.hocr'):
|
||||
# Tesseract 3.03 appends suffix ".hocr" on its own (.badxml.hocr)
|
||||
shutil.move(badxml + '.hocr', badxml)
|
||||
|
||||
# Tesseract 3.03 inserts source filename into hocr file without
|
||||
# escaping it, creating invalid XML and breaking the parser.
|
||||
# As a workaround, rewrite the hocr file, replacing the filename
|
||||
# with a space. Don't know if Tesseract 3.02 does the same.
|
||||
|
||||
regex_nested_single_quotes = re.compile(
|
||||
r"""title='image "([^"]*)";""")
|
||||
with open(badxml, mode='r', encoding='utf-8') as f_in, \
|
||||
open(output_hocr, mode='w', encoding='utf-8') as f_out:
|
||||
for line in f_in:
|
||||
line = regex_nested_single_quotes.sub(
|
||||
r"""title='image " ";""", line)
|
||||
f_out.write(line)
|
||||
|
||||
|
||||
def generate_pdf(input_image, skip_pdf, output_pdf, language: list,
|
||||
tessconfig: list, timeout: float, pagesegmode: int, log):
|
||||
'''Use Tesseract to render a PDF.
|
||||
|
||||
input_image -- image to analyze
|
||||
skip_pdf -- if we time out, use this file as output
|
||||
language -- list of languages to consider
|
||||
tessconfig -- tesseract configuration
|
||||
timeout -- timeout (seconds)
|
||||
log -- logger object
|
||||
'''
|
||||
|
||||
args_tesseract = [
|
||||
get_program('tesseract'),
|
||||
'-l', '+'.join(language)
|
||||
]
|
||||
|
||||
if pagesegmode is not None:
|
||||
args_tesseract.extend(['-psm', str(pagesegmode)])
|
||||
|
||||
args_tesseract.extend([
|
||||
input_image,
|
||||
os.path.splitext(output_pdf)[0], # Tesseract appends suffix
|
||||
'pdf'
|
||||
] + tessconfig)
|
||||
|
||||
try:
|
||||
stdout = check_output(
|
||||
args_tesseract, close_fds=True, stderr=STDOUT,
|
||||
universal_newlines=True, timeout=timeout)
|
||||
except TimeoutExpired:
|
||||
page_timedout(log, input_image)
|
||||
shutil.copy(skip_pdf, output_pdf)
|
||||
except CalledProcessError as e:
|
||||
tesseract_log_output(log, e.output, input_image)
|
||||
if 'Image too large' in e.output:
|
||||
shutil.copy(skip_pdf, output_pdf)
|
||||
return
|
||||
raise e from e
|
||||
else:
|
||||
tesseract_log_output(log, stdout, input_image)
|
||||
@@ -1,103 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# © 2015 James R. Barlow: github.com/jbarlow83
|
||||
# unpaper documentation:
|
||||
# https://github.com/Flameeyes/unpaper/blob/master/doc/basic-concepts.md
|
||||
|
||||
from subprocess import CalledProcessError, STDOUT, check_output, check_call
|
||||
from tempfile import NamedTemporaryFile
|
||||
import sys
|
||||
import os
|
||||
from functools import lru_cache
|
||||
from . import ExitCode, get_program
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def version():
|
||||
args_unpaper = [
|
||||
get_program('unpaper'),
|
||||
'--version'
|
||||
]
|
||||
version = check_output(
|
||||
args_unpaper, close_fds=True, universal_newlines=True,
|
||||
stderr=STDOUT, timeout=5)
|
||||
return version.strip()
|
||||
|
||||
|
||||
try:
|
||||
from PIL import Image
|
||||
except ImportError:
|
||||
print("Could not find Python3 imaging library", file=sys.stderr)
|
||||
raise
|
||||
|
||||
|
||||
def run(input_file, output_file, dpi, log, mode_args):
|
||||
args_unpaper = [
|
||||
get_program('unpaper'),
|
||||
'-v',
|
||||
'--dpi', str(dpi)
|
||||
] + mode_args
|
||||
|
||||
SUFFIXES = {'1': '.pbm', 'L': '.pgm', 'RGB': '.ppm'}
|
||||
|
||||
im = Image.open(input_file)
|
||||
if im.mode not in SUFFIXES.keys():
|
||||
log.info("Converting image to other colorspace")
|
||||
try:
|
||||
if im.mode == 'P' and len(im.getcolors()) == 2:
|
||||
im = im.convert(mode='1')
|
||||
else:
|
||||
im = im.convert(mode='RGB')
|
||||
except IOError:
|
||||
log.error(
|
||||
"Could not convert image with type " + im.mode)
|
||||
sys.exit(ExitCode.missing_dependency)
|
||||
|
||||
try:
|
||||
suffix = SUFFIXES[im.mode]
|
||||
except KeyError:
|
||||
log.error(
|
||||
"Failed to convert image to a supported format.")
|
||||
sys.exit(ExitCode.missing_dependency)
|
||||
|
||||
with NamedTemporaryFile(suffix=suffix) as input_pnm, \
|
||||
NamedTemporaryFile(suffix=suffix, mode="r+b") as output_pnm:
|
||||
im.save(input_pnm, format='PPM')
|
||||
im.close()
|
||||
|
||||
os.unlink(output_pnm.name)
|
||||
|
||||
args_unpaper.extend([input_pnm.name, output_pnm.name])
|
||||
try:
|
||||
stdout = check_output(
|
||||
args_unpaper, close_fds=True,
|
||||
universal_newlines=True, stderr=STDOUT,
|
||||
)
|
||||
except CalledProcessError as e:
|
||||
log.debug(e.output)
|
||||
raise e from e
|
||||
else:
|
||||
log.debug(stdout)
|
||||
Image.open(output_pnm.name).save(output_file)
|
||||
|
||||
|
||||
def deskew(input_file, output_file, dpi, log):
|
||||
run(input_file, output_file, dpi, log, [
|
||||
'--mask-scan-size', '100', # don't blank out narrow columns
|
||||
'--no-border-align', # don't align visible content to borders
|
||||
'--no-mask-center', # don't center visible content within page
|
||||
'--no-grayfilter', # don't remove light gray areas
|
||||
'--no-blackfilter', # don't remove solid black areas
|
||||
'--no-noisefilter', # don't remove salt and pepper noise
|
||||
'--no-blurfilter' # don't remove blurry objects/debris
|
||||
])
|
||||
|
||||
|
||||
def clean(input_file, output_file, dpi, log):
|
||||
run(input_file, output_file, dpi, log, [
|
||||
'--mask-scan-size', '100', # don't blank out narrow columns
|
||||
'--no-border-align', # don't align visible content to borders
|
||||
'--no-mask-center', # don't center visible content within page
|
||||
'--no-grayfilter', # don't remove light gray areas
|
||||
'--no-blackfilter', # don't remove solid black areas
|
||||
'--no-deskew', # don't deskew
|
||||
])
|
||||
@@ -1,338 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 2.38.0 (20140413.2041)
|
||||
-->
|
||||
<!-- Title: Pipeline: Pages: 1 -->
|
||||
<svg width="1444pt" height="973pt"
|
||||
viewBox="0.00 0.00 1444.00 973.18" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 969.181)">
|
||||
<title>Pipeline:</title>
|
||||
<polygon fill="white" stroke="none" points="-4,4 -4,-969.181 1440,-969.181 1440,4 -4,4"/>
|
||||
<g id="clust1" class="cluster"><title>clustertasks</title>
|
||||
<polygon fill="none" stroke="black" points="8,-8 8,-957.181 1428,-957.181 1428,-8 8,-8"/>
|
||||
<text text-anchor="middle" x="718" y="-929.181" font-family="Times,serif" font-size="30.00" fill="#ff3232">Pipeline:</text>
|
||||
</g>
|
||||
<!-- t0 -->
|
||||
<g id="node1" class="node"><title>t0</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="1219.77,-911.181 1034.23,-911.181 1030.23,-907.181 1030.23,-875.181 1215.77,-875.181 1219.77,-879.181 1219.77,-911.181"/>
|
||||
<polyline fill="none" stroke="black" points="1215.77,-907.181 1030.23,-907.181 "/>
|
||||
<polyline fill="none" stroke="black" points="1215.77,-907.181 1215.77,-875.181 "/>
|
||||
<polyline fill="none" stroke="black" points="1215.77,-907.181 1219.77,-911.181 "/>
|
||||
<text text-anchor="middle" x="1125" y="-887.181" font-family="Times,serif" font-size="20.00">triage</text>
|
||||
</g>
|
||||
<!-- t1 -->
|
||||
<g id="node2" class="node"><title>t1</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="1238.54,-853.181 1015.46,-853.181 1011.46,-849.181 1011.46,-817.181 1234.54,-817.181 1238.54,-821.181 1238.54,-853.181"/>
|
||||
<polyline fill="none" stroke="black" points="1234.54,-849.181 1011.46,-849.181 "/>
|
||||
<polyline fill="none" stroke="black" points="1234.54,-849.181 1234.54,-817.181 "/>
|
||||
<polyline fill="none" stroke="black" points="1234.54,-849.181 1238.54,-853.181 "/>
|
||||
<text text-anchor="middle" x="1125" y="-829.181" font-family="Times,serif" font-size="20.00">repair_pdf</text>
|
||||
</g>
|
||||
<!-- t0->t1 -->
|
||||
<g id="edge1" class="edge"><title>t0->t1</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1125,-875.075C1125,-871.384 1125,-867.394 1125,-863.424"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1128.5,-863.182 1125,-853.182 1121.5,-863.182 1128.5,-863.182"/>
|
||||
</g>
|
||||
<!-- t2 -->
|
||||
<g id="node3" class="node"><title>t2</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="1329.11,-774.253 1125,-791.156 920.888,-774.253 921.078,-746.905 1328.92,-746.905 1329.11,-774.253"/>
|
||||
<polygon fill="none" stroke="black" points="1333.13,-777.932 1125,-795.167 916.866,-777.932 917.11,-742.906 1332.89,-742.906 1333.13,-777.932"/>
|
||||
<text text-anchor="middle" x="1125" y="-760.694" font-family="Times,serif" font-size="20.00">split_pages</text>
|
||||
</g>
|
||||
<!-- t1->t2 -->
|
||||
<g id="edge2" class="edge"><title>t1->t2</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1125,-817.059C1125,-813.44 1125,-809.484 1125,-805.438"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1128.5,-805.334 1125,-795.334 1121.5,-805.334 1128.5,-805.334"/>
|
||||
</g>
|
||||
<!-- t15 -->
|
||||
<g id="node17" class="node"><title>t15</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="632.082,-716.208 293.918,-716.208 289.918,-712.208 289.918,-680.208 628.082,-680.208 632.082,-684.208 632.082,-716.208"/>
|
||||
<polyline fill="none" stroke="black" points="628.082,-712.208 289.918,-712.208 "/>
|
||||
<polyline fill="none" stroke="black" points="628.082,-712.208 628.082,-680.208 "/>
|
||||
<polyline fill="none" stroke="black" points="628.082,-712.208 632.082,-716.208 "/>
|
||||
<text text-anchor="middle" x="461" y="-692.208" font-family="Times,serif" font-size="20.00">generate_postscript_stub</text>
|
||||
</g>
|
||||
<!-- t1->t15 -->
|
||||
<g id="edge23" class="edge"><title>t1->t15</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1026.3,-817.165C989.157,-810.572 946.587,-802.788 908,-795.181 781.899,-770.319 636.479,-738.51 546.633,-718.49"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="547.289,-715.05 536.767,-716.289 545.765,-721.882 547.289,-715.05"/>
|
||||
</g>
|
||||
<!-- t18 -->
|
||||
<g id="node19" class="node"><title>t18</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="1025.79,-156.452 1221,-129.134 1416.21,-156.452 1416.03,-200.654 1025.97,-200.654 1025.79,-156.452"/>
|
||||
<polygon fill="none" stroke="black" points="1021.78,-152.971 1221,-125.091 1420.22,-152.971 1420.01,-204.657 1021.99,-204.657 1021.78,-152.971"/>
|
||||
<text text-anchor="middle" x="1221" y="-162.669" font-family="Times,serif" font-size="20.00">merge_pages_qpdf</text>
|
||||
</g>
|
||||
<!-- t1->t18 -->
|
||||
<g id="edge33" class="edge"><title>t1->t18</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1238.67,-826.639C1321.4,-814.236 1419,-782.049 1419,-699.208 1419,-699.208 1419,-699.208 1419,-319.208 1419,-280.294 1424,-263.161 1398,-234.208 1389.64,-224.894 1379.82,-216.923 1369.17,-210.101"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1370.83,-207.019 1360.46,-204.872 1367.23,-213.02 1370.83,-207.019"/>
|
||||
</g>
|
||||
<!-- t3 -->
|
||||
<g id="node4" class="node"><title>t3</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="1186.88,-716.208 905.117,-716.208 901.117,-712.208 901.117,-680.208 1182.88,-680.208 1186.88,-684.208 1186.88,-716.208"/>
|
||||
<polyline fill="none" stroke="black" points="1182.88,-712.208 901.117,-712.208 "/>
|
||||
<polyline fill="none" stroke="black" points="1182.88,-712.208 1182.88,-680.208 "/>
|
||||
<polyline fill="none" stroke="black" points="1182.88,-712.208 1186.88,-716.208 "/>
|
||||
<text text-anchor="middle" x="1044" y="-692.208" font-family="Times,serif" font-size="20.00">rasterize_preview</text>
|
||||
</g>
|
||||
<!-- t2->t3 -->
|
||||
<g id="edge3" class="edge"><title>t2->t3</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1097.12,-742.813C1089.24,-736.345 1080.66,-729.296 1072.73,-722.788"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1074.71,-719.888 1064.76,-716.249 1070.27,-725.299 1074.71,-719.888"/>
|
||||
</g>
|
||||
<!-- t4 -->
|
||||
<g id="node5" class="node"><title>t4</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="1258.15,-658.208 1023.85,-658.208 1019.85,-654.208 1019.85,-622.208 1254.15,-622.208 1258.15,-626.208 1258.15,-658.208"/>
|
||||
<polyline fill="none" stroke="black" points="1254.15,-654.208 1019.85,-654.208 "/>
|
||||
<polyline fill="none" stroke="black" points="1254.15,-654.208 1254.15,-622.208 "/>
|
||||
<polyline fill="none" stroke="black" points="1254.15,-654.208 1258.15,-658.208 "/>
|
||||
<text text-anchor="middle" x="1139" y="-634.208" font-family="Times,serif" font-size="20.00">orient_page</text>
|
||||
</g>
|
||||
<!-- t2->t4 -->
|
||||
<g id="edge5" class="edge"><title>t2->t4</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1171.65,-742.795C1181.42,-735.692 1190.44,-726.864 1196,-716.208 1203.4,-702.023 1203.46,-694.36 1196,-680.208 1192.84,-674.222 1188.31,-668.98 1183.15,-664.445"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1185.3,-661.676 1175.25,-658.302 1181,-667.203 1185.3,-661.676"/>
|
||||
</g>
|
||||
<!-- t3->t4 -->
|
||||
<g id="edge4" class="edge"><title>t3->t4</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1073.02,-680.102C1081.95,-674.836 1091.92,-668.963 1101.34,-663.407"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1103.32,-666.302 1110.16,-658.209 1099.77,-660.272 1103.32,-666.302"/>
|
||||
</g>
|
||||
<!-- t5 -->
|
||||
<g id="node6" class="node"><title>t5</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="1271.3,-600.208 920.701,-600.208 916.701,-596.208 916.701,-564.208 1267.3,-564.208 1271.3,-568.208 1271.3,-600.208"/>
|
||||
<polyline fill="none" stroke="black" points="1267.3,-596.208 916.701,-596.208 "/>
|
||||
<polyline fill="none" stroke="black" points="1267.3,-596.208 1267.3,-564.208 "/>
|
||||
<polyline fill="none" stroke="black" points="1267.3,-596.208 1271.3,-600.208 "/>
|
||||
<text text-anchor="middle" x="1094" y="-576.208" font-family="Times,serif" font-size="20.00">rasterize_with_ghostscript</text>
|
||||
</g>
|
||||
<!-- t4->t5 -->
|
||||
<g id="edge6" class="edge"><title>t4->t5</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1125.25,-622.102C1121.76,-617.759 1117.94,-613.003 1114.21,-608.353"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1116.65,-605.812 1107.66,-600.209 1111.2,-610.197 1116.65,-605.812"/>
|
||||
</g>
|
||||
<!-- t10 -->
|
||||
<g id="node12" class="node"><title>t10</title>
|
||||
<polygon fill="#00cc66" stroke="black" points="334,-348.208 35.7812,-320.208 334,-292.208 632.219,-320.208 334,-348.208"/>
|
||||
<text text-anchor="middle" x="334" y="-314.208" font-family="Times,serif" font-size="20.00">select_image_layer</text>
|
||||
</g>
|
||||
<!-- t4->t10 -->
|
||||
<g id="edge15" class="edge"><title>t4->t10</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1019.87,-634.085C799.18,-622.991 349,-592.156 349,-525.208 349,-525.208 349,-525.208 349,-465.208 349,-428.606 344.147,-387.052 339.961,-358.022"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="343.393,-357.303 338.46,-347.926 336.469,-358.332 343.393,-357.303"/>
|
||||
</g>
|
||||
<!-- t16 -->
|
||||
<g id="node15" class="node"><title>t16</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="1380.34,-270.208 1159.66,-270.208 1155.66,-266.208 1155.66,-234.208 1376.34,-234.208 1380.34,-238.208 1380.34,-270.208"/>
|
||||
<polyline fill="none" stroke="black" points="1376.34,-266.208 1155.66,-266.208 "/>
|
||||
<polyline fill="none" stroke="black" points="1376.34,-266.208 1376.34,-234.208 "/>
|
||||
<polyline fill="none" stroke="black" points="1376.34,-266.208 1380.34,-270.208 "/>
|
||||
<text text-anchor="middle" x="1268" y="-246.208" font-family="Times,serif" font-size="20.00">skip_page</text>
|
||||
</g>
|
||||
<!-- t4->t16 -->
|
||||
<g id="edge20" class="edge"><title>t4->t16</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1258.35,-631.473C1326.7,-619.809 1399,-591.745 1399,-525.208 1399,-525.208 1399,-525.208 1399,-397.208 1399,-350.33 1418.37,-328.748 1389,-292.208 1383.58,-285.469 1376.99,-279.879 1369.71,-275.244"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1371.3,-272.123 1360.88,-270.231 1367.85,-278.211 1371.3,-272.123"/>
|
||||
</g>
|
||||
<!-- t14 -->
|
||||
<g id="node16" class="node"><title>t14</title>
|
||||
<polygon fill="#66ccff" stroke="black" points="1380.24,-338.208 999.76,-338.208 995.76,-334.208 995.76,-302.208 1376.24,-302.208 1380.24,-306.208 1380.24,-338.208"/>
|
||||
<polyline fill="none" stroke="black" points="1376.24,-334.208 995.76,-334.208 "/>
|
||||
<polyline fill="none" stroke="black" points="1376.24,-334.208 1376.24,-302.208 "/>
|
||||
<polyline fill="none" stroke="black" points="1376.24,-334.208 1380.24,-338.208 "/>
|
||||
<text text-anchor="middle" x="1188" y="-314.208" font-family="Times,serif" font-size="20.00">tesseract_ocr_and_render_pdf</text>
|
||||
</g>
|
||||
<!-- t4->t14 -->
|
||||
<g id="edge22" class="edge"><title>t4->t14</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1244.5,-622.179C1297.25,-607.003 1350,-578.503 1350,-525.208 1350,-525.208 1350,-525.208 1350,-465.208 1350,-422.752 1366.76,-403.169 1340,-370.208 1330.4,-358.377 1317.91,-349.368 1304.22,-342.513"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1305.62,-339.304 1295.07,-338.313 1302.7,-345.666 1305.62,-339.304"/>
|
||||
</g>
|
||||
<!-- t6 -->
|
||||
<g id="node7" class="node"><title>t6</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="1074.74,-542.208 779.258,-542.208 775.258,-538.208 775.258,-506.208 1070.74,-506.208 1074.74,-510.208 1074.74,-542.208"/>
|
||||
<polyline fill="none" stroke="black" points="1070.74,-538.208 775.258,-538.208 "/>
|
||||
<polyline fill="none" stroke="black" points="1070.74,-538.208 1070.74,-506.208 "/>
|
||||
<polyline fill="none" stroke="black" points="1070.74,-538.208 1074.74,-542.208 "/>
|
||||
<text text-anchor="middle" x="925" y="-518.208" font-family="Times,serif" font-size="20.00">preprocess_deskew</text>
|
||||
</g>
|
||||
<!-- t5->t6 -->
|
||||
<g id="edge7" class="edge"><title>t5->t6</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1042.38,-564.102C1024.52,-558.185 1004.35,-551.5 985.823,-545.362"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="986.901,-542.032 976.308,-542.209 984.699,-548.677 986.901,-542.032"/>
|
||||
</g>
|
||||
<!-- t9 -->
|
||||
<g id="node11" class="node"><title>t9</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="1013,-426.208 694.762,-398.208 1013,-370.208 1331.24,-398.208 1013,-426.208"/>
|
||||
<text text-anchor="middle" x="1013" y="-392.208" font-family="Times,serif" font-size="20.00">select_image_for_pdf</text>
|
||||
</g>
|
||||
<!-- t5->t9 -->
|
||||
<g id="edge13" class="edge"><title>t5->t9</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1093.89,-563.996C1093.29,-548.44 1091.11,-525.146 1084,-506.208 1073.94,-479.426 1056.13,-452.609 1040.86,-432.521"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1043.62,-430.369 1034.72,-424.619 1038.09,-434.663 1043.62,-430.369"/>
|
||||
</g>
|
||||
<!-- t7 -->
|
||||
<g id="node8" class="node"><title>t7</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="993.95,-484.208 716.05,-484.208 712.05,-480.208 712.05,-448.208 989.95,-448.208 993.95,-452.208 993.95,-484.208"/>
|
||||
<polyline fill="none" stroke="black" points="989.95,-480.208 712.05,-480.208 "/>
|
||||
<polyline fill="none" stroke="black" points="989.95,-480.208 989.95,-448.208 "/>
|
||||
<polyline fill="none" stroke="black" points="989.95,-480.208 993.95,-484.208 "/>
|
||||
<text text-anchor="middle" x="853" y="-460.208" font-family="Times,serif" font-size="20.00">preprocess_clean</text>
|
||||
</g>
|
||||
<!-- t6->t7 -->
|
||||
<g id="edge8" class="edge"><title>t6->t7</title>
|
||||
<path fill="none" stroke="#0044a0" d="M903.006,-506.102C896.726,-501.216 889.771,-495.807 883.095,-490.615"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="884.901,-487.586 874.859,-484.209 880.604,-493.111 884.901,-487.586"/>
|
||||
</g>
|
||||
<!-- t6->t9 -->
|
||||
<g id="edge12" class="edge"><title>t6->t9</title>
|
||||
<path fill="none" stroke="#0044a0" d="M977.454,-506.003C987.329,-500.504 996.562,-493.377 1003,-484.208 1012.67,-470.441 1015.74,-452.229 1016.16,-436.195"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1019.66,-436.018 1016.09,-426.041 1012.66,-436.063 1019.66,-436.018"/>
|
||||
</g>
|
||||
<!-- t8 -->
|
||||
<g id="node9" class="node"><title>t8</title>
|
||||
<polygon fill="#00cc66" stroke="black" points="676.666,-416.208 387.334,-416.208 383.334,-412.208 383.334,-380.208 672.666,-380.208 676.666,-384.208 676.666,-416.208"/>
|
||||
<polyline fill="none" stroke="black" points="672.666,-412.208 383.334,-412.208 "/>
|
||||
<polyline fill="none" stroke="black" points="672.666,-412.208 672.666,-380.208 "/>
|
||||
<polyline fill="none" stroke="black" points="672.666,-412.208 676.666,-416.208 "/>
|
||||
<text text-anchor="middle" x="530" y="-392.208" font-family="Times,serif" font-size="20.00">ocr_tesseract_hocr</text>
|
||||
</g>
|
||||
<!-- t7->t8 -->
|
||||
<g id="edge9" class="edge"><title>t7->t8</title>
|
||||
<path fill="none" stroke="#0044a0" d="M769.399,-448.125C724.645,-438.98 669.394,-427.691 623.252,-418.262"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="623.748,-414.791 613.25,-416.219 622.347,-421.65 623.748,-414.791"/>
|
||||
</g>
|
||||
<!-- t7->t9 -->
|
||||
<g id="edge11" class="edge"><title>t7->t9</title>
|
||||
<path fill="none" stroke="#0044a0" d="M894.62,-448.039C911.413,-441.112 931.162,-432.966 949.666,-425.333"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="951.009,-428.565 958.918,-421.516 948.339,-422.094 951.009,-428.565"/>
|
||||
</g>
|
||||
<!-- t11 -->
|
||||
<g id="node10" class="node"><title>t11</title>
|
||||
<polygon fill="#00cc66" stroke="black" points="938.109,-338.208 653.891,-338.208 649.891,-334.208 649.891,-302.208 934.109,-302.208 938.109,-306.208 938.109,-338.208"/>
|
||||
<polyline fill="none" stroke="black" points="934.109,-334.208 649.891,-334.208 "/>
|
||||
<polyline fill="none" stroke="black" points="934.109,-334.208 934.109,-302.208 "/>
|
||||
<polyline fill="none" stroke="black" points="934.109,-334.208 938.109,-338.208 "/>
|
||||
<text text-anchor="middle" x="794" y="-314.208" font-family="Times,serif" font-size="20.00">render_hocr_page</text>
|
||||
</g>
|
||||
<!-- t8->t11 -->
|
||||
<g id="edge10" class="edge"><title>t8->t11</title>
|
||||
<path fill="none" stroke="#0044a0" d="M589.247,-380.152C629.442,-368.58 682.727,-353.241 724.863,-341.111"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="725.853,-344.468 734.495,-338.338 723.917,-337.741 725.853,-344.468"/>
|
||||
</g>
|
||||
<!-- t12 -->
|
||||
<g id="node14" class="node"><title>t12</title>
|
||||
<polygon fill="#00cc66" stroke="black" points="1118.49,-270.208 775.514,-270.208 771.514,-266.208 771.514,-234.208 1114.49,-234.208 1118.49,-238.208 1118.49,-270.208"/>
|
||||
<polyline fill="none" stroke="black" points="1114.49,-266.208 771.514,-266.208 "/>
|
||||
<polyline fill="none" stroke="black" points="1114.49,-266.208 1114.49,-234.208 "/>
|
||||
<polyline fill="none" stroke="black" points="1114.49,-266.208 1118.49,-270.208 "/>
|
||||
<text text-anchor="middle" x="945" y="-246.208" font-family="Times,serif" font-size="20.00">render_hocr_debug_page</text>
|
||||
</g>
|
||||
<!-- t8->t12 -->
|
||||
<g id="edge19" class="edge"><title>t8->t12</title>
|
||||
<path fill="none" stroke="#0044a0" d="M620.537,-380.161C641.947,-376.512 664.736,-372.939 686,-370.208 714.866,-366.5 927.304,-369.633 947,-348.208 963.502,-330.256 960.281,-301.363 954.611,-280.149"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="957.925,-279.012 951.707,-270.433 951.218,-281.017 957.925,-279.012"/>
|
||||
</g>
|
||||
<!-- t13 -->
|
||||
<g id="node13" class="node"><title>t13</title>
|
||||
<polygon fill="#00cc66" stroke="black" points="753.082,-270.208 494.918,-270.208 490.918,-266.208 490.918,-234.208 749.082,-234.208 753.082,-238.208 753.082,-270.208"/>
|
||||
<polyline fill="none" stroke="black" points="749.082,-266.208 490.918,-266.208 "/>
|
||||
<polyline fill="none" stroke="black" points="749.082,-266.208 749.082,-234.208 "/>
|
||||
<polyline fill="none" stroke="black" points="749.082,-266.208 753.082,-270.208 "/>
|
||||
<text text-anchor="middle" x="622" y="-246.208" font-family="Times,serif" font-size="20.00">add_text_layer</text>
|
||||
</g>
|
||||
<!-- t11->t13 -->
|
||||
<g id="edge16" class="edge"><title>t11->t13</title>
|
||||
<path fill="none" stroke="#0044a0" d="M749.258,-302.039C726.862,-293.446 699.574,-282.975 676.019,-273.936"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="677.108,-270.605 666.518,-270.29 674.6,-277.14 677.108,-270.605"/>
|
||||
</g>
|
||||
<!-- t9->t10 -->
|
||||
<g id="edge14" class="edge"><title>t9->t10</title>
|
||||
<path fill="none" stroke="#0044a0" d="M877.633,-382.056C761.19,-369.023 593.788,-350.286 475.578,-337.054"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="475.697,-333.546 465.37,-335.912 474.919,-340.503 475.697,-333.546"/>
|
||||
</g>
|
||||
<!-- t9->t12 -->
|
||||
<g id="edge18" class="edge"><title>t9->t12</title>
|
||||
<path fill="none" stroke="#0044a0" d="M998.887,-371.447C994.931,-364.003 990.702,-355.831 987,-348.208 983.822,-341.663 967.96,-305.616 956.669,-279.865"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="959.82,-278.337 952.601,-270.582 953.409,-281.146 959.82,-278.337"/>
|
||||
</g>
|
||||
<!-- t9->t14 -->
|
||||
<g id="edge21" class="edge"><title>t9->t14</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1065.03,-374.611C1088.59,-364.381 1116.27,-352.359 1139.4,-342.314"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1140.9,-345.479 1148.68,-338.285 1138.11,-339.058 1140.9,-345.479"/>
|
||||
</g>
|
||||
<!-- t10->t13 -->
|
||||
<g id="edge17" class="edge"><title>t10->t13</title>
|
||||
<path fill="none" stroke="#0044a0" d="M417.688,-300.029C455.345,-291.399 499.718,-281.231 537.473,-272.578"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="538.461,-275.943 547.427,-270.297 536.898,-269.12 538.461,-275.943"/>
|
||||
</g>
|
||||
<!-- t17 -->
|
||||
<g id="node18" class="node"><title>t17</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="540.724,-156.455 770,-129.143 999.276,-156.455 999.062,-200.646 540.938,-200.646 540.724,-156.455"/>
|
||||
<polygon fill="none" stroke="black" points="536.704,-152.903 770,-125.112 1003.3,-152.903 1003.05,-204.648 536.954,-204.648 536.704,-152.903"/>
|
||||
<text text-anchor="middle" x="770" y="-162.669" font-family="Times,serif" font-size="20.00">merge_pages_ghostscript</text>
|
||||
</g>
|
||||
<!-- t13->t17 -->
|
||||
<g id="edge27" class="edge"><title>t13->t17</title>
|
||||
<path fill="none" stroke="#0044a0" d="M653.026,-234.114C665.992,-226.971 681.653,-218.343 697.24,-209.755"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="699.319,-212.606 706.388,-204.715 695.941,-206.475 699.319,-212.606"/>
|
||||
</g>
|
||||
<!-- t13->t18 -->
|
||||
<g id="edge31" class="edge"><title>t13->t18</title>
|
||||
<path fill="none" stroke="#0044a0" d="M753.329,-235.235C756.584,-234.884 759.812,-234.541 763,-234.208 873.497,-222.668 902.059,-228.201 1012,-212.208 1023.74,-210.5 1035.81,-208.554 1047.95,-206.456"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1048.91,-209.84 1058.16,-204.659 1047.7,-202.946 1048.91,-209.84"/>
|
||||
</g>
|
||||
<!-- t12->t17 -->
|
||||
<g id="edge28" class="edge"><title>t12->t17</title>
|
||||
<path fill="none" stroke="#0044a0" d="M908.314,-234.114C892.552,-226.77 873.424,-217.858 854.484,-209.033"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="855.759,-205.766 845.216,-204.715 852.803,-212.111 855.759,-205.766"/>
|
||||
</g>
|
||||
<!-- t12->t18 -->
|
||||
<g id="edge32" class="edge"><title>t12->t18</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1002.86,-234.114C1029.07,-226.37 1061.19,-216.881 1092.64,-207.59"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1093.77,-210.905 1102.37,-204.715 1091.79,-204.192 1093.77,-210.905"/>
|
||||
</g>
|
||||
<!-- t16->t17 -->
|
||||
<g id="edge24" class="edge"><title>t16->t17</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1155.57,-234.546C1111.13,-227.821 1059.63,-219.853 1013,-212.208 1001.53,-210.326 989.752,-208.365 977.872,-206.363"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="978.316,-202.888 967.872,-204.672 977.149,-209.79 978.316,-202.888"/>
|
||||
</g>
|
||||
<!-- t16->t18 -->
|
||||
<g id="edge29" class="edge"><title>t16->t18</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1258.03,-233.915C1254.58,-227.933 1250.55,-220.928 1246.43,-213.785"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1249.24,-211.645 1241.21,-204.729 1243.17,-215.141 1249.24,-211.645"/>
|
||||
</g>
|
||||
<!-- t14->t17 -->
|
||||
<g id="edge26" class="edge"><title>t14->t17</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1172.36,-302.154C1164.33,-293.013 1154.66,-281.362 1147,-270.208 1136.64,-255.121 1141.88,-244.862 1127,-234.208 1125.19,-232.915 1055.67,-220.232 978.45,-206.446"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="978.964,-202.982 968.505,-204.672 977.735,-209.874 978.964,-202.982"/>
|
||||
</g>
|
||||
<!-- t14->t18 -->
|
||||
<g id="edge30" class="edge"><title>t14->t18</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1310.22,-302.184C1345.8,-294.579 1378.07,-284.119 1389,-270.208 1408.78,-245.028 1391.25,-224.765 1361.2,-209.211"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1362.48,-205.944 1351.96,-204.742 1359.44,-212.246 1362.48,-205.944"/>
|
||||
</g>
|
||||
<!-- t15->t17 -->
|
||||
<g id="edge25" class="edge"><title>t15->t17</title>
|
||||
<path fill="none" stroke="#0044a0" d="M289.656,-684.797C164.646,-670.853 17,-641.95 17,-583.208 17,-583.208 17,-583.208 17,-319.208 17,-213.417 306.554,-181.327 526.553,-172.213"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="526.794,-175.706 536.646,-171.808 526.514,-168.711 526.794,-175.706"/>
|
||||
</g>
|
||||
<!-- t19 -->
|
||||
<g id="node20" class="node"><title>t19</title>
|
||||
<polygon fill="#efa03b" stroke="black" points="844.361,-47.3406 995,-20.006 1145.64,-47.3406 1145.5,-91.5689 844.501,-91.5689 844.361,-47.3406"/>
|
||||
<polygon fill="none" stroke="black" points="840.355,-43.9965 995,-15.9349 1149.65,-43.9965 1149.48,-95.5749 840.519,-95.5749 840.355,-43.9965"/>
|
||||
<text text-anchor="middle" x="995" y="-53.565" font-family="Times,serif" font-size="20.00">copy_final</text>
|
||||
</g>
|
||||
<!-- t17->t19 -->
|
||||
<g id="edge35" class="edge"><title>t17->t19</title>
|
||||
<path fill="none" stroke="#0044a0" d="M841.81,-133.486C864.177,-122.839 889.024,-111.011 912.085,-100.034"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="913.64,-103.17 921.165,-95.7118 910.631,-96.8497 913.64,-103.17"/>
|
||||
</g>
|
||||
<!-- t18->t19 -->
|
||||
<g id="edge34" class="edge"><title>t18->t19</title>
|
||||
<path fill="none" stroke="#0044a0" d="M1151.35,-134.66C1128.23,-123.705 1102.26,-111.399 1078.23,-100.009"/>
|
||||
<polygon fill="#0044a0" stroke="#0044a0" points="1079.66,-96.8131 1069.12,-95.6933 1076.66,-103.139 1079.66,-96.8131"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 23 KiB |
@@ -0,0 +1,82 @@
|
||||
[build-system]
|
||||
requires = [
|
||||
"setuptools >= 30.3.0",
|
||||
"wheel",
|
||||
"cffi",
|
||||
"setuptools_scm[toml] >= 3.4",
|
||||
"setuptools_scm_git_archive"
|
||||
]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.setuptools_scm]
|
||||
version_scheme = "post-release"
|
||||
|
||||
[tool.black]
|
||||
line-length = 88
|
||||
target-version = ["py36", "py37", "py38"]
|
||||
skip-string-normalization = true
|
||||
include = '\.pyi?$'
|
||||
exclude = '''
|
||||
/(
|
||||
\.eggs
|
||||
| \.git
|
||||
| \.hg
|
||||
| \.mypy_cache
|
||||
| \.tox
|
||||
| \.venv
|
||||
| _build
|
||||
| buck-out
|
||||
| build
|
||||
| dist
|
||||
| docs
|
||||
| misc
|
||||
| \.egg-info
|
||||
| src/ocrmypdf/lib/_leptonica.py
|
||||
)/
|
||||
'''
|
||||
|
||||
[tool.coverage.run]
|
||||
branch = true
|
||||
parallel = true
|
||||
concurrency = ["multiprocessing"]
|
||||
|
||||
[tool.coverage.paths]
|
||||
source = ["src/ocrmypdf"]
|
||||
|
||||
[tool.coverage.report]
|
||||
# Regexes for lines to exclude from consideration
|
||||
exclude_lines = [
|
||||
# Have to re-enable the standard pragma
|
||||
"pragma: no cover",
|
||||
|
||||
# Don't complain if tests don't hit defensive assertion code:
|
||||
"raise AssertionError",
|
||||
"raise NotImplementedError",
|
||||
|
||||
# Don't complain if non-runnable code isn't run:
|
||||
"if 0:",
|
||||
"if False:",
|
||||
"if __name__ == .__main__.:",
|
||||
"if TYPE_CHECKING:"
|
||||
]
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
known_first_party = "ocrmypdf"
|
||||
known_third_party = ["PIL", "_cffi_backend", "cffi", "flask", "img2pdf", "ocrmypdf", "pdfminer", "pikepdf", "pkg_resources", "pluggy", "pytest", "reportlab", "setuptools", "sphinx_rtd_theme", "tqdm", "watchdog", "werkzeug"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
minversion = "6.0"
|
||||
norecursedirs = ["lib", ".pc", ".git", "venv", "output", "cache", "resources"]
|
||||
testpaths = ["tests"]
|
||||
addopts = "-n auto"
|
||||
markers = ["slow"]
|
||||
filterwarnings = ["ignore:.*XMLParser.*:DeprecationWarning"]
|
||||
|
||||
[tool.mypy]
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = [
|
||||
'pluggy', 'tqdm', 'coloredlogs', 'img2pdf', 'cffi', '_cffi_backend', 'pdfminer.*', 'reportlab.*'
|
||||
]
|
||||
ignore_missing_imports = true
|
||||
@@ -1,2 +0,0 @@
|
||||
[pytest]
|
||||
norecursedirs = lib .pc .git
|
||||
@@ -1,9 +0,0 @@
|
||||
# requirements.txt can be used to replicate the developer's build environment
|
||||
# setup.py lists a separate set of requirements that are looser to simplify
|
||||
# installation
|
||||
ruffus==2.6.3
|
||||
Pillow==3.3.0
|
||||
reportlab==3.2.0
|
||||
PyPDF2==1.26
|
||||
img2pdf==0.2.1
|
||||
cffi==1.5.2
|
||||
@@ -0,0 +1,10 @@
|
||||
# Deprecated and not maintained; use "pip install ocrmypdf" instead
|
||||
cffi == 1.14.5
|
||||
coloredlogs == 15.0 # technically optional
|
||||
img2pdf == 0.4.0
|
||||
pdfminer.six == 20201018
|
||||
pikepdf == 2.10.0
|
||||
pluggy == 0.13.1
|
||||
Pillow == 8.3.2
|
||||
reportlab == 3.5.66
|
||||
tqdm == 4.59.0
|
||||
@@ -0,0 +1,7 @@
|
||||
# Deprecated and not maintained; use "pip install ocrmypdf[test]" instead
|
||||
pytest >= 6.0.0
|
||||
pytest-xdist >= 2.2.0
|
||||
pytest-cov >= 2.11.1
|
||||
python-xmp-toolkit == 2.0.1 # requires apt-get install libexempi3
|
||||
# or brew install exempi
|
||||
#PyMuPDF == 1.13.4 # optional
|
||||
@@ -0,0 +1,2 @@
|
||||
# Deprecated and not maintained; use "pip install ocrmypdf[watcher]" instead
|
||||
watchdog == 1.0.2
|
||||
@@ -0,0 +1,2 @@
|
||||
# Deprecated and not maintained; use "pip install ocrmypdf[webservice]" instead
|
||||
Flask >= 1, < 2
|
||||
@@ -1,9 +1,115 @@
|
||||
[metadata]
|
||||
name = ocrmypdf
|
||||
description = OCRmyPDF adds an OCR text layer to scanned PDF files, allowing them to be searched
|
||||
long_description = file: README.md
|
||||
long_description_content_type = text/markdown
|
||||
url = https://github.com/jbarlow83/OCRmyPDF
|
||||
author = James R. Barlow
|
||||
author_email = james@purplerock.ca
|
||||
license = MPL-2.0
|
||||
license_file = LICENSE
|
||||
license_files =
|
||||
LICENSE
|
||||
classifiers =
|
||||
Development Status :: 5 - Production/Stable
|
||||
Environment :: Console
|
||||
Intended Audience :: End Users/Desktop
|
||||
Intended Audience :: Science/Research
|
||||
Intended Audience :: System Administrators
|
||||
License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)
|
||||
Operating System :: MacOS :: MacOS X
|
||||
Operating System :: Microsoft :: Windows :: Windows 10
|
||||
Operating System :: POSIX
|
||||
Operating System :: POSIX :: BSD
|
||||
Operating System :: POSIX :: Linux
|
||||
Programming Language :: Python :: 3
|
||||
Programming Language :: Python :: 3 :: Only
|
||||
Programming Language :: Python :: 3.6
|
||||
Programming Language :: Python :: 3.7
|
||||
Programming Language :: Python :: 3.8
|
||||
Programming Language :: Python :: 3.9
|
||||
Topic :: Scientific/Engineering :: Image Recognition
|
||||
Topic :: Text Processing :: Indexing
|
||||
Topic :: Text Processing :: Linguistic
|
||||
keywords =
|
||||
PDF
|
||||
OCR
|
||||
optical character recognition
|
||||
PDF/A
|
||||
scanning
|
||||
project_urls =
|
||||
Documentation = https://ocrmypdf.readthedocs.io/
|
||||
Source = https://github.com/jbarlow83/ocrmypdf
|
||||
Tracker = https://github.com/jbarlow83/ocrmypdf/issues
|
||||
|
||||
[options]
|
||||
packages = find:
|
||||
install_requires =
|
||||
Pillow>=8.2.0
|
||||
cffi>=1.9.1 # must be a setup and install requirement
|
||||
coloredlogs>=14.0 # strictly optional
|
||||
img2pdf>=0.3.0,<0.5 # pure Python
|
||||
importlib-metadata>=4;python_version<'3.8' # until Python 3.8
|
||||
importlib-resources>=5;python_version<'3.9' # until Python 3.9
|
||||
pdfminer.six!=20200720,>=20191110,<=20201018
|
||||
pikepdf>=2.10.0
|
||||
pikepdf<3;implementation_name=="pypy" and python_version=='3.6'
|
||||
pluggy>=0.13.0,<2
|
||||
reportlab>=3.5.66
|
||||
tqdm>=4
|
||||
python_requires = >=3.6
|
||||
include_package_data = True
|
||||
package_dir =
|
||||
=src
|
||||
platforms = any
|
||||
setup_requires =
|
||||
cffi>=1.9.1 # to build the leptonica module
|
||||
setuptools_scm
|
||||
setuptools_scm_git_archive
|
||||
zip_safe = False
|
||||
|
||||
[options.packages.find]
|
||||
where = src
|
||||
|
||||
[options.entry_points]
|
||||
console_scripts =
|
||||
ocrmypdf = ocrmypdf.__main__:run
|
||||
|
||||
[options.extras_require]
|
||||
docs =
|
||||
sphinx
|
||||
sphinx-issues
|
||||
sphinx-rtd-theme
|
||||
extended_test =
|
||||
PyMuPDF==1.13.4
|
||||
test =
|
||||
coverage[toml]>=5
|
||||
pytest>=6.0.0
|
||||
pytest-cov>=2.11.1
|
||||
pytest-xdist>=2.2.0
|
||||
python-xmp-toolkit==2.0.1 # also requires apt-get install libexempi3
|
||||
watcher =
|
||||
watchdog>=1.0.2,<3
|
||||
webservice =
|
||||
Flask>=1,<3
|
||||
|
||||
[options.package_data]
|
||||
ocrmypdf =
|
||||
data/sRGB.icc
|
||||
py.typed
|
||||
|
||||
[bdist_wheel]
|
||||
python-tag = py34
|
||||
python-tag = py36
|
||||
|
||||
[aliases]
|
||||
test=pytest
|
||||
test = pytest
|
||||
|
||||
[check-manifest]
|
||||
ignore =
|
||||
.github
|
||||
.github
|
||||
|
||||
[flake8]
|
||||
ignore = D203,F401,W503,E501,E203,F841
|
||||
exclude = .git,__pycache__,docs/conf.py,build,dist,.venv,.venvpp,.eggs,tmp,src/ocrmypdf/lib/
|
||||
max-complexity = 10
|
||||
max-line-length = 100
|
||||
|
||||
@@ -1,238 +1,19 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# © 2015 James R. Barlow: github.com/jbarlow83
|
||||
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import sys
|
||||
if sys.version_info < (3, 4):
|
||||
print("Python 3.4 or newer is required", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
from setuptools import setup # nopep8
|
||||
from subprocess import STDOUT, check_output, CalledProcessError # nopep8
|
||||
from collections.abc import Mapping # nopep8
|
||||
import re # nopep8
|
||||
# © 2021 James R. Barlow: github.com/jbarlow83
|
||||
#
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
missing_program = '''
|
||||
The program '{program}' could not be executed or was not found on your
|
||||
system PATH.
|
||||
'''
|
||||
|
||||
unknown_version = '''
|
||||
OCRmyPDF requires '{program}' {need_version} or higher. Your system has
|
||||
'{program}' but we cannot tell what version is installed. Contact the
|
||||
package maintainer.
|
||||
'''
|
||||
|
||||
old_version = '''
|
||||
OCRmyPDF requires '{program}' {need_version} or higher. Your system appears
|
||||
to have {found_version}. Please update this program.
|
||||
'''
|
||||
|
||||
okay_its_optional = '''
|
||||
This program is OPTIONAL, so installation of OCRmyPDF can proceed, but
|
||||
some functionality may be missing.
|
||||
'''
|
||||
|
||||
not_okay_its_required = '''
|
||||
This program is REQUIRED for OCRmyPDF to work. Installation will abort.
|
||||
'''
|
||||
|
||||
osx_install_advice = '''
|
||||
If you have homebrew installed, try these command to install the missing
|
||||
packages:
|
||||
brew update
|
||||
brew upgrade
|
||||
brew install {package}
|
||||
'''
|
||||
|
||||
linux_install_advice = '''
|
||||
On systems with the aptitude package manager (Debian, Ubuntu), try these
|
||||
commands:
|
||||
sudo apt-get update
|
||||
sudo apt-get install {package}
|
||||
|
||||
On RPM-based systems (Red Hat, Fedora), search for instructions on
|
||||
installing the RPM for {program}.
|
||||
'''
|
||||
|
||||
|
||||
def get_platform():
|
||||
if sys.platform.startswith('freebsd'):
|
||||
return 'freebsd'
|
||||
elif sys.platform.startswith('linux'):
|
||||
return 'linux'
|
||||
return sys.platform
|
||||
|
||||
|
||||
def _error_trailer(program, package, optional, **kwargs):
|
||||
if optional:
|
||||
print(okay_its_optional.format(**locals()), file=sys.stderr)
|
||||
else:
|
||||
print(not_okay_its_required.format(**locals()), file=sys.stderr)
|
||||
|
||||
if isinstance(package, Mapping):
|
||||
package = package[get_platform()]
|
||||
|
||||
if get_platform() == 'darwin':
|
||||
print(osx_install_advice.format(**locals()), file=sys.stderr)
|
||||
elif get_platform() == 'linux':
|
||||
print(linux_install_advice.format(**locals()), file=sys.stderr)
|
||||
|
||||
|
||||
def error_missing_program(
|
||||
program,
|
||||
package,
|
||||
optional
|
||||
):
|
||||
print(missing_program.format(**locals()), file=sys.stderr)
|
||||
_error_trailer(**locals())
|
||||
|
||||
|
||||
def error_unknown_version(
|
||||
program,
|
||||
package,
|
||||
optional,
|
||||
need_version
|
||||
):
|
||||
print(unknown_version.format(**locals()), file=sys.stderr)
|
||||
_error_trailer(**locals())
|
||||
|
||||
|
||||
def error_old_version(
|
||||
program,
|
||||
package,
|
||||
optional,
|
||||
need_version,
|
||||
found_version
|
||||
):
|
||||
print(old_version.format(**locals()), file=sys.stderr)
|
||||
_error_trailer(**locals())
|
||||
|
||||
|
||||
def check_external_program(
|
||||
program,
|
||||
need_version,
|
||||
package,
|
||||
version_check_args=['--version'],
|
||||
version_scrape_regex=re.compile(r'(\d+\.\d+(?:\.\d+)?)'),
|
||||
optional=False):
|
||||
|
||||
print('Checking for {program} >= {need_version}...'.format(
|
||||
program=program, need_version=need_version))
|
||||
try:
|
||||
result = check_output(
|
||||
[program] + version_check_args,
|
||||
universal_newlines=True, stderr=STDOUT)
|
||||
except (CalledProcessError, FileNotFoundError):
|
||||
error_missing_program(program, package, optional)
|
||||
if not optional:
|
||||
sys.exit(1)
|
||||
print('Continuing install without {program}'.format(program=program))
|
||||
return
|
||||
|
||||
try:
|
||||
found_version = version_scrape_regex.search(result).group(1)
|
||||
except AttributeError:
|
||||
error_unknown_version(program, package, optional, need_version)
|
||||
sys.exit(1)
|
||||
|
||||
if found_version < need_version:
|
||||
error_old_version(program, package, optional, need_version,
|
||||
found_version)
|
||||
|
||||
print('Found {program} {found_version}'.format(
|
||||
program=program, found_version=found_version))
|
||||
|
||||
|
||||
command = next((arg for arg in sys.argv[1:] if not arg.startswith('-')), '')
|
||||
forced = ('--force' in sys.argv)
|
||||
|
||||
|
||||
if not forced and command.startswith('install') or \
|
||||
command in ['check', 'test', 'nosetests', 'easy_install']:
|
||||
check_external_program(
|
||||
program='tesseract',
|
||||
need_version='3.03', # limited by Travis CI / Ubuntu 12.04 backports
|
||||
package={'darwin': 'tesseract', 'linux': 'tesseract-ocr'}
|
||||
)
|
||||
check_external_program(
|
||||
program='gs',
|
||||
need_version='9.15', # limited by Travis CI / Ubuntu 12.04 backports
|
||||
package='ghostscript'
|
||||
)
|
||||
check_external_program(
|
||||
program='unpaper',
|
||||
need_version='6.1', # latest sane version
|
||||
package='unpaper',
|
||||
optional=True
|
||||
)
|
||||
check_external_program(
|
||||
program='qpdf',
|
||||
need_version='5.0.0', # limited by Travis CI / Ubuntu 12.04 backports
|
||||
package='qpdf',
|
||||
version_check_args=['--version']
|
||||
)
|
||||
else:
|
||||
print('Skipping external program tests because of --force')
|
||||
|
||||
|
||||
if 'upload' in sys.argv[1:]:
|
||||
print('Use twine to upload the package - setup.py upload is insecure')
|
||||
sys.exit(1)
|
||||
|
||||
tests_require = open('test_requirements.txt').read().splitlines()
|
||||
from setuptools import setup
|
||||
|
||||
# Minimal setup to support older setuptools/setuptools_scm
|
||||
setup(
|
||||
name='ocrmypdf',
|
||||
description='OCRmyPDF adds an OCR text layer to scanned PDF files, allowing them to be searched',
|
||||
url='https://github.com/jbarlow83/OCRmyPDF',
|
||||
author='James R. Barlow',
|
||||
author_email='jim@purplerock.ca',
|
||||
license='Public Domain',
|
||||
packages=['ocrmypdf'],
|
||||
keywords=['PDF', 'OCR', 'optical character recognition', 'PDF/A', 'scanning'],
|
||||
classifiers=[
|
||||
"Programming Language :: Python :: 3",
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Environment :: Console",
|
||||
"Intended Audience :: End Users/Desktop",
|
||||
"Intended Audience :: Science/Research",
|
||||
"Intended Audience :: System Administrators",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: MacOS :: MacOS X",
|
||||
"Operating System :: POSIX",
|
||||
"Operating System :: POSIX :: BSD",
|
||||
"Operating System :: POSIX :: Linux",
|
||||
"Topic :: Scientific/Engineering :: Image Recognition",
|
||||
"Topic :: Text Processing :: Indexing",
|
||||
"Topic :: Text Processing :: Linguistic",
|
||||
],
|
||||
setup_requires=[
|
||||
'setuptools_scm',
|
||||
'cffi>=1.5.0',
|
||||
'pytest-runner'
|
||||
setup_requires=[ # can be removed whenever we can drop pip 9 support
|
||||
'cffi >= 1.9.1', # to build the leptonica module
|
||||
'setuptools_scm', # so that version will work
|
||||
'setuptools_scm_git_archive', # enable version from github tarballs
|
||||
],
|
||||
use_scm_version={'version_scheme': 'post-release'},
|
||||
cffi_modules=[
|
||||
'ocrmypdf/lib/compile_leptonica.py:ffi'
|
||||
],
|
||||
install_requires=[
|
||||
'ruffus==2.6.3', # pinned - ocrmypdf implements a 2.6.3 workaround
|
||||
'Pillow>=3.1.0', # Pillow is pretty stable
|
||||
'reportlab>=3.2.0', # oldest released version with sane image handling
|
||||
'PyPDF2>=1.26', # pure Python, so track HEAD closely
|
||||
'img2pdf>=0.2.1', # pure Python, so track HEAD closely
|
||||
'cffi>=1.5.0' # oldest version ever tested
|
||||
],
|
||||
tests_require=tests_require,
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'ocrmypdf = ocrmypdf.__main__:run_pipeline'
|
||||
],
|
||||
},
|
||||
package_data={'ocrmypdf': ['data/sRGB.icc']},
|
||||
include_package_data=True,
|
||||
zip_safe=False)
|
||||
cffi_modules=['src/ocrmypdf/lib/compile_leptonica.py:ffibuilder'],
|
||||
)
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
deb http://httpredir.debian.org/debian stretch main
|
||||
deb http://httpredir.debian.org/debian stretch-updates main
|
||||
deb http://security.debian.org stretch/updates main
|
||||
deb http://ftp.de.debian.org/debian sid main contrib non-free
|
||||
@@ -0,0 +1,35 @@
|
||||
# Release checklist
|
||||
|
||||
## Patch release
|
||||
|
||||
- Check `pytest`
|
||||
|
||||
- Update release notes
|
||||
|
||||
## Minor release
|
||||
|
||||
## Major release
|
||||
|
||||
- Run `pre-commit autoupdate`
|
||||
|
||||
- Check README.md
|
||||
|
||||
- Check setup.py
|
||||
|
||||
- Are classifiers up to date?
|
||||
- Is `python_requires` correct?
|
||||
- Python 3.6 is EOL on December 2021-12. Could drop support then.
|
||||
- Can we tighten any `install_requires` dependencies?
|
||||
|
||||
- Search for old version shims we can remove
|
||||
|
||||
- "shim"
|
||||
- ` pikepdf.__version__`
|
||||
|
||||
- Search for deprecation: search all files for deprec*, etc.
|
||||
|
||||
- Check requirements in setup.cfg
|
||||
|
||||
- Delete `tests/cache`, do `pytest --runslow`, and update cache.
|
||||
|
||||
- Do `pytest --cov-report html`
|
||||
@@ -0,0 +1,32 @@
|
||||
# © 2017 James R. Barlow: github.com/jbarlow83
|
||||
#
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
from pluggy import HookimplMarker as _HookimplMarker
|
||||
|
||||
from ocrmypdf import helpers, hocrtransform, leptonica, pdfa, pdfinfo
|
||||
from ocrmypdf._concurrent import Executor
|
||||
from ocrmypdf._jobcontext import PageContext, PdfContext
|
||||
from ocrmypdf._version import PROGRAM_NAME, __version__
|
||||
from ocrmypdf.api import Verbosity, configure_logging, ocr
|
||||
from ocrmypdf.exceptions import (
|
||||
BadArgsError,
|
||||
DpiError,
|
||||
EncryptedPdfError,
|
||||
ExitCode,
|
||||
ExitCodeException,
|
||||
InputFileError,
|
||||
MissingDependencyError,
|
||||
OutputFileAccessError,
|
||||
PdfMergeFailedError,
|
||||
PriorOcrFoundError,
|
||||
SubprocessOutputError,
|
||||
TesseractConfigError,
|
||||
UnsupportedImageFormatError,
|
||||
)
|
||||
from ocrmypdf.pluginspec import OcrEngine, OrientationConfidence
|
||||
|
||||
hookimpl = _HookimplMarker('ocrmypdf')
|
||||
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env python3
|
||||
# © 2015-19 James R. Barlow: github.com/jbarlow83
|
||||
#
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
from multiprocessing import set_start_method
|
||||
|
||||
from ocrmypdf import __version__
|
||||
from ocrmypdf._plugin_manager import get_parser_options_plugins
|
||||
from ocrmypdf._sync import run_pipeline
|
||||
from ocrmypdf._validation import check_closed_streams, check_options
|
||||
from ocrmypdf.api import Verbosity, configure_logging
|
||||
from ocrmypdf.exceptions import (
|
||||
BadArgsError,
|
||||
ExitCode,
|
||||
InputFileError,
|
||||
MissingDependencyError,
|
||||
)
|
||||
|
||||
log = logging.getLogger('ocrmypdf')
|
||||
|
||||
|
||||
def sigbus(*args):
|
||||
raise InputFileError("Lost access to the input file")
|
||||
|
||||
|
||||
def run(args=None):
|
||||
_parser, options, plugin_manager = get_parser_options_plugins(args=args)
|
||||
|
||||
if not check_closed_streams(options):
|
||||
return ExitCode.bad_args
|
||||
|
||||
if hasattr(os, 'nice'):
|
||||
os.nice(5)
|
||||
|
||||
verbosity = options.verbose
|
||||
if not os.isatty(sys.stderr.fileno()):
|
||||
options.progress_bar = False
|
||||
if options.quiet:
|
||||
verbosity = Verbosity.quiet
|
||||
options.progress_bar = False
|
||||
configure_logging(
|
||||
verbosity,
|
||||
progress_bar_friendly=options.progress_bar,
|
||||
manage_root_logger=True,
|
||||
plugin_manager=plugin_manager,
|
||||
)
|
||||
log.debug('ocrmypdf %s', __version__)
|
||||
try:
|
||||
check_options(options, plugin_manager)
|
||||
except ValueError as e:
|
||||
log.error(e)
|
||||
return ExitCode.bad_args
|
||||
except BadArgsError as e:
|
||||
log.error(e)
|
||||
return e.exit_code
|
||||
except MissingDependencyError as e:
|
||||
log.error(e)
|
||||
return ExitCode.missing_dependency
|
||||
|
||||
if hasattr(signal, 'SIGBUS'):
|
||||
signal.signal(signal.SIGBUS, sigbus)
|
||||
|
||||
result = run_pipeline(options=options, plugin_manager=plugin_manager)
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if sys.platform == 'darwin' and sys.version_info < (3, 8):
|
||||
set_start_method('spawn') # see python bpo-33725
|
||||
sys.exit(run())
|
||||
@@ -0,0 +1,133 @@
|
||||
# © 2020 James R. Barlow: github.com/jbarlow83
|
||||
#
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
import threading
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Callable, Iterable, Optional
|
||||
|
||||
|
||||
def _task_noop(*_args, **_kwargs):
|
||||
return
|
||||
|
||||
|
||||
class NullProgressBar:
|
||||
def __init__(self, **kwargs):
|
||||
pass
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
return False
|
||||
|
||||
def update(self, _arg=None):
|
||||
return
|
||||
|
||||
|
||||
class Executor(ABC):
|
||||
pool_lock = threading.Lock()
|
||||
pbar_class = NullProgressBar
|
||||
|
||||
def __init__(self, *, pbar_class=None):
|
||||
if pbar_class:
|
||||
self.pbar_class = pbar_class
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
*,
|
||||
use_threads: bool,
|
||||
max_workers: int,
|
||||
tqdm_kwargs: dict,
|
||||
worker_initializer: Optional[Callable] = None,
|
||||
task: Optional[Callable] = None,
|
||||
task_arguments: Optional[Iterable] = None,
|
||||
task_finished: Optional[Callable] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Set up parallel execution and progress reporting.
|
||||
|
||||
Args:
|
||||
use_threads: If ``False``, the workload is the sort that will benefit from
|
||||
running in a multiprocessing context (for example, it uses Python
|
||||
heavily, and parallelizing it with threads is not expected to be
|
||||
performant).
|
||||
max_workers: The maximum number of workers that should be run.
|
||||
tdqm_kwargs: Arguments to set up the progress bar.
|
||||
worker_initializer: Called when a worker is initialized, in the worker's
|
||||
execution context. If the child workers are processes, it must be
|
||||
possible to marshall/pickle the worker initializer.
|
||||
``functools.partial`` can be used to bind parameters.
|
||||
task: Called when the worker starts a new task, in the worker's execution
|
||||
context. Must be possible to marshall to the worker.
|
||||
task_finished: Called when a worker finishes a task, in the parent's
|
||||
context.
|
||||
task_arguments: An iterable that generates a group of parameters for each
|
||||
task. This runs in the parent's context, but the parameters must be
|
||||
marshallable to the worker.
|
||||
"""
|
||||
|
||||
if not task_arguments:
|
||||
return # Nothing to do!
|
||||
if not worker_initializer:
|
||||
worker_initializer = _task_noop
|
||||
if not task_finished:
|
||||
task_finished = _task_noop
|
||||
if not task:
|
||||
task = _task_noop
|
||||
|
||||
with self.pool_lock:
|
||||
self._execute(
|
||||
use_threads=use_threads,
|
||||
max_workers=max_workers,
|
||||
tqdm_kwargs=tqdm_kwargs,
|
||||
worker_initializer=worker_initializer,
|
||||
task=task,
|
||||
task_arguments=task_arguments,
|
||||
task_finished=task_finished,
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
def _execute(
|
||||
self,
|
||||
*,
|
||||
use_threads: bool,
|
||||
max_workers: int,
|
||||
tqdm_kwargs: dict,
|
||||
worker_initializer: Callable,
|
||||
task: Callable,
|
||||
task_arguments: Iterable,
|
||||
task_finished: Callable,
|
||||
):
|
||||
"""Custom executors should override this method."""
|
||||
|
||||
|
||||
def setup_executor(plugin_manager) -> Executor:
|
||||
pbar_class = plugin_manager.hook.get_progressbar_class()
|
||||
return plugin_manager.hook.get_executor(progressbar_class=pbar_class)
|
||||
|
||||
|
||||
class SerialExecutor(Executor):
|
||||
"""Implements a purely sequential executor using the parallel protocol.
|
||||
|
||||
The current process/thread will be the worker that executes all tasks
|
||||
in order. As such, ``worker_initializer`` will never be called.
|
||||
"""
|
||||
|
||||
def _execute(
|
||||
self,
|
||||
*,
|
||||
use_threads: bool,
|
||||
max_workers: int,
|
||||
tqdm_kwargs: dict,
|
||||
worker_initializer: Callable,
|
||||
task: Callable,
|
||||
task_arguments: Iterable,
|
||||
task_finished: Callable,
|
||||
): # pylint: disable=unused-argument
|
||||
with self.pbar_class(**tqdm_kwargs) as pbar:
|
||||
for args in task_arguments:
|
||||
result = task(args)
|
||||
task_finished(result, pbar)
|
||||
@@ -0,0 +1,8 @@
|
||||
# © 2020 James R. Barlow: github.com/jbarlow83
|
||||
#
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
"""Manage third party executables"""
|
||||
@@ -0,0 +1,270 @@
|
||||
# © 2017 James R. Barlow: github.com/jbarlow83
|
||||
#
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
"""Interface to Ghostscript executable"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from io import BytesIO
|
||||
from os import fspath
|
||||
from pathlib import Path
|
||||
from shutil import which
|
||||
from subprocess import PIPE, CalledProcessError
|
||||
from typing import Optional
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from ocrmypdf.exceptions import MissingDependencyError, SubprocessOutputError
|
||||
from ocrmypdf.helpers import Resolution
|
||||
from ocrmypdf.subprocess import get_version, run, run_polling_stderr
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
missing_gs_error = """
|
||||
---------------------------------------------------------------------
|
||||
This error normally occurs when ocrmypdf find can't Ghostscript.
|
||||
Please ensure Ghostscript is installed and its location is added to
|
||||
the system PATH environment variable.
|
||||
|
||||
For details see:
|
||||
https://ocrmypdf.readthedocs.io/en/latest/installation.html
|
||||
---------------------------------------------------------------------
|
||||
"""
|
||||
|
||||
_gswin = None
|
||||
if os.name == 'nt':
|
||||
_gswin = which('gswin64c')
|
||||
if not _gswin:
|
||||
_gswin = which('gswin32c')
|
||||
if not _gswin:
|
||||
raise MissingDependencyError(missing_gs_error)
|
||||
_gswin = Path(_gswin).stem
|
||||
|
||||
GS = _gswin if _gswin else 'gs'
|
||||
del _gswin
|
||||
|
||||
|
||||
def version():
|
||||
return get_version(GS)
|
||||
|
||||
|
||||
def jpeg_passthrough_available() -> bool:
|
||||
"""Returns True if the installed version of Ghostscript supports JPEG passthru
|
||||
|
||||
Prior to 9.23, Ghostscript decoded and re-encoded JPEGs internally. In 9.23
|
||||
it gained the ability to keep JPEGs unmodified. However, the 9.23
|
||||
implementation was buggy and would deletes the last two bytes of images in
|
||||
some cases, as reported here.
|
||||
https://bugs.ghostscript.com/show_bug.cgi?id=699216
|
||||
|
||||
The issue was fixed for 9.24, hence that is the first version we consider
|
||||
the feature available. (Ghostscript 9.24 has its own problems is blacklisted.)
|
||||
"""
|
||||
return version() >= '9.24'
|
||||
|
||||
|
||||
def _gs_error_reported(stream) -> bool:
|
||||
return True if re.search(r'error', stream, flags=re.IGNORECASE) else False
|
||||
|
||||
|
||||
def rasterize_pdf(
|
||||
input_file: os.PathLike,
|
||||
output_file: os.PathLike,
|
||||
*,
|
||||
raster_device: str,
|
||||
raster_dpi: Resolution,
|
||||
pageno: int = 1,
|
||||
page_dpi: Optional[Resolution] = None,
|
||||
rotation: Optional[int] = None,
|
||||
filter_vector: bool = False,
|
||||
):
|
||||
"""Rasterize one page of a PDF at resolution raster_dpi in canvas units."""
|
||||
raster_dpi = raster_dpi.round(6)
|
||||
if not page_dpi:
|
||||
page_dpi = raster_dpi
|
||||
|
||||
args_gs = (
|
||||
[
|
||||
GS,
|
||||
'-dQUIET',
|
||||
'-dSAFER',
|
||||
'-dBATCH',
|
||||
'-dNOPAUSE',
|
||||
f'-sDEVICE={raster_device}',
|
||||
f'-dFirstPage={pageno}',
|
||||
f'-dLastPage={pageno}',
|
||||
f'-r{raster_dpi.x:f}x{raster_dpi.y:f}',
|
||||
]
|
||||
+ (['-dFILTERVECTOR'] if filter_vector else [])
|
||||
+ [
|
||||
'-o',
|
||||
'-',
|
||||
'-sstdout=%stderr',
|
||||
'-dAutoRotatePages=/None', # Probably has no effect on raster
|
||||
'-f',
|
||||
fspath(input_file),
|
||||
]
|
||||
)
|
||||
|
||||
try:
|
||||
p = run(args_gs, stdout=PIPE, stderr=PIPE, check=True)
|
||||
except CalledProcessError as e:
|
||||
log.error(e.stderr.decode(errors='replace'))
|
||||
raise SubprocessOutputError('Ghostscript rasterizing failed')
|
||||
else:
|
||||
stderr = p.stderr.decode(errors='replace')
|
||||
if _gs_error_reported(stderr):
|
||||
log.error(stderr)
|
||||
|
||||
with Image.open(BytesIO(p.stdout)) as im:
|
||||
if rotation is not None:
|
||||
log.debug("Rotating output by %i", rotation)
|
||||
# rotation is a clockwise angle and Image.ROTATE_* is
|
||||
# counterclockwise so this cancels out the rotation
|
||||
if rotation == 90:
|
||||
im = im.transpose(Image.ROTATE_90)
|
||||
elif rotation == 180:
|
||||
im = im.transpose(Image.ROTATE_180)
|
||||
elif rotation == 270:
|
||||
im = im.transpose(Image.ROTATE_270)
|
||||
if rotation % 180 == 90:
|
||||
page_dpi = page_dpi.flip_axis()
|
||||
im.save(fspath(output_file), dpi=page_dpi)
|
||||
|
||||
|
||||
class GhostscriptFollower:
|
||||
re_process = re.compile(r"Processing pages \d+ through (\d+).")
|
||||
re_page = re.compile(r"Page (\d+)")
|
||||
|
||||
def __init__(self, progressbar_class):
|
||||
self.count = 0
|
||||
self.progressbar_class = progressbar_class
|
||||
self.progressbar = None
|
||||
|
||||
def __call__(self, line):
|
||||
if not self.progressbar_class:
|
||||
return
|
||||
if not self.progressbar:
|
||||
m = self.re_process.match(line.strip())
|
||||
if m:
|
||||
self.count = int(m.group(1))
|
||||
self.progressbar = self.progressbar_class(
|
||||
total=self.count, desc="PDF/A conversion", unit='page'
|
||||
)
|
||||
return
|
||||
else:
|
||||
m = self.re_page.match(line.strip())
|
||||
if m:
|
||||
self.progressbar.update()
|
||||
|
||||
|
||||
def generate_pdfa(
|
||||
pdf_pages,
|
||||
output_file: os.PathLike,
|
||||
*,
|
||||
compression: str,
|
||||
pdf_version: str = '1.5',
|
||||
pdfa_part: str = '2',
|
||||
progressbar_class=None,
|
||||
):
|
||||
# Ghostscript's compression is all or nothing. We can either force all images
|
||||
# to JPEG, force all to Flate/PNG, or let it decide how to encode the images.
|
||||
# In most case it's best to let it decide.
|
||||
compression_args = []
|
||||
if compression == 'jpeg':
|
||||
compression_args = [
|
||||
"-dAutoFilterColorImages=false",
|
||||
"-dColorImageFilter=/DCTEncode",
|
||||
"-dAutoFilterGrayImages=false",
|
||||
"-dGrayImageFilter=/DCTEncode",
|
||||
]
|
||||
elif compression == 'lossless':
|
||||
compression_args = [
|
||||
"-dAutoFilterColorImages=false",
|
||||
"-dColorImageFilter=/FlateEncode",
|
||||
"-dAutoFilterGrayImages=false",
|
||||
"-dGrayImageFilter=/FlateEncode",
|
||||
]
|
||||
else:
|
||||
compression_args = [
|
||||
"-dAutoFilterColorImages=true",
|
||||
"-dAutoFilterGrayImages=true",
|
||||
]
|
||||
|
||||
strategy = 'LeaveColorUnchanged'
|
||||
# Older versions of Ghostscript expect a leading slash in
|
||||
# sColorConversionStrategy, newer ones should not have it. See Ghostscript
|
||||
# git commit fe1c025d.
|
||||
strategy = ('/' + strategy) if version() < '9.19' else strategy
|
||||
|
||||
if version() == '9.23':
|
||||
# 9.23: added JPEG passthrough as a new feature, but with a bug that
|
||||
# incorrectly formats some images. Fixed as of 9.24. So we disable this
|
||||
# feature for 9.23.
|
||||
# https://bugs.ghostscript.com/show_bug.cgi?id=699216
|
||||
compression_args.append('-dPassThroughJPEGImages=false')
|
||||
|
||||
# nb no need to specify ProcessColorModel when ColorConversionStrategy
|
||||
# is set; see:
|
||||
# https://bugs.ghostscript.com/show_bug.cgi?id=699392
|
||||
args_gs = (
|
||||
[
|
||||
GS,
|
||||
"-dBATCH",
|
||||
"-dNOPAUSE",
|
||||
"-dSAFER",
|
||||
"-dCompatibilityLevel=" + str(pdf_version),
|
||||
"-sDEVICE=pdfwrite",
|
||||
"-dAutoRotatePages=/None",
|
||||
"-sColorConversionStrategy=" + strategy,
|
||||
]
|
||||
+ compression_args
|
||||
+ [
|
||||
"-dJPEGQ=95",
|
||||
"-dPDFA=" + pdfa_part,
|
||||
"-dPDFACompatibilityPolicy=1",
|
||||
"-o",
|
||||
"-",
|
||||
"-sstdout=%stderr",
|
||||
]
|
||||
)
|
||||
args_gs.extend(fspath(s) for s in pdf_pages) # Stringify Path objs
|
||||
|
||||
try:
|
||||
with Path(output_file).open('wb') as output:
|
||||
p = run_polling_stderr(
|
||||
args_gs,
|
||||
stdout=output,
|
||||
stderr=PIPE,
|
||||
check=True,
|
||||
text=True,
|
||||
encoding='utf-8',
|
||||
errors='replace',
|
||||
callback=GhostscriptFollower(progressbar_class),
|
||||
)
|
||||
except CalledProcessError as e:
|
||||
# Ghostscript does not change return code when it fails to create
|
||||
# PDF/A - check PDF/A status elsewhere
|
||||
log.error(e.stderr)
|
||||
raise SubprocessOutputError('Ghostscript PDF/A rendering failed') from e
|
||||
else:
|
||||
stderr = p.stderr
|
||||
# If there is an error we log the whole stderr, except for filtering
|
||||
# duplicates.
|
||||
if _gs_error_reported(stderr):
|
||||
last_part = None
|
||||
repcount = 0
|
||||
for part in stderr.split('****'):
|
||||
if part != last_part:
|
||||
if repcount > 1:
|
||||
log.error(f"(previous error message repeated {repcount} times)")
|
||||
repcount = 0
|
||||
log.error(part)
|
||||
else:
|
||||
repcount += 1
|
||||
last_part = part
|
||||
@@ -0,0 +1,57 @@
|
||||
# © 2018 James R. Barlow: github.com/jbarlow83
|
||||
#
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
"""Interface to jbig2 executable"""
|
||||
|
||||
from subprocess import PIPE
|
||||
|
||||
from ocrmypdf.exceptions import MissingDependencyError
|
||||
from ocrmypdf.subprocess import get_version, run
|
||||
|
||||
|
||||
def version():
|
||||
return get_version('jbig2', regex=r'jbig2enc (\d+(\.\d+)*).*')
|
||||
|
||||
|
||||
def available():
|
||||
try:
|
||||
version()
|
||||
except MissingDependencyError:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def convert_group(*, cwd, infiles, out_prefix):
|
||||
args = [
|
||||
'jbig2',
|
||||
'-b',
|
||||
out_prefix,
|
||||
'-s', # symbol mode (lossy)
|
||||
# '-r', # refinement mode (lossless symbol mode, currently disabled in
|
||||
# jbig2)
|
||||
'-p',
|
||||
]
|
||||
args.extend(infiles)
|
||||
proc = run(args, cwd=cwd, stdout=PIPE, stderr=PIPE)
|
||||
proc.check_returncode()
|
||||
return proc
|
||||
|
||||
|
||||
def convert_group_mp(args):
|
||||
return convert_group(cwd=args[0], infiles=args[1], out_prefix=args[2])
|
||||
|
||||
|
||||
def convert_single(*, cwd, infile, outfile):
|
||||
args = ['jbig2', '-p', infile]
|
||||
with open(outfile, 'wb') as fstdout:
|
||||
proc = run(args, cwd=cwd, stdout=fstdout, stderr=PIPE)
|
||||
proc.check_returncode()
|
||||
return proc
|
||||
|
||||
|
||||
def convert_single_mp(args):
|
||||
return convert_single(cwd=args[0], infile=args[1], outfile=args[2])
|
||||
@@ -0,0 +1,65 @@
|
||||
# © 2018 James R. Barlow: github.com/jbarlow83
|
||||
#
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
"""Interface to pngquant executable"""
|
||||
|
||||
from contextlib import contextmanager
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from subprocess import PIPE
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from ocrmypdf.exceptions import MissingDependencyError
|
||||
from ocrmypdf.subprocess import get_version, run
|
||||
|
||||
|
||||
def version():
|
||||
return get_version('pngquant', regex=r'(\d+(\.\d+)*).*')
|
||||
|
||||
|
||||
def available():
|
||||
try:
|
||||
version()
|
||||
except MissingDependencyError:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
@contextmanager
|
||||
def input_as_png(input_file: Path):
|
||||
if not input_file.name.endswith('.png'):
|
||||
with Image.open(input_file) as im:
|
||||
bio = BytesIO()
|
||||
im.save(bio, format='png')
|
||||
bio.seek(0)
|
||||
yield bio
|
||||
else:
|
||||
with open(input_file, 'rb') as f:
|
||||
yield f
|
||||
|
||||
|
||||
def quantize(input_file: Path, output_file: Path, quality_min: int, quality_max: int):
|
||||
with input_as_png(input_file) as input_stream:
|
||||
args = [
|
||||
'pngquant',
|
||||
'--force',
|
||||
'--skip-if-larger',
|
||||
'--quality',
|
||||
f'{quality_min}-{quality_max}',
|
||||
'--', # pngquant: stop processing arguments
|
||||
'-', # pngquant: stream input and output
|
||||
]
|
||||
result = run(args, stdin=input_stream, stdout=PIPE, stderr=PIPE, check=False)
|
||||
|
||||
if result.returncode == 0:
|
||||
# input_file could be the same as output_file, so we defer the write
|
||||
output_file.write_bytes(result.stdout)
|
||||
|
||||
|
||||
def quantize_mp(args):
|
||||
return quantize(*args)
|
||||
@@ -0,0 +1,345 @@
|
||||
# © 2017 James R. Barlow: github.com/jbarlow83
|
||||
#
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
"""Interface to Tesseract executable"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
from collections import namedtuple
|
||||
from distutils.version import StrictVersion
|
||||
from os import fspath
|
||||
from pathlib import Path
|
||||
from subprocess import PIPE, STDOUT, CalledProcessError, TimeoutExpired
|
||||
from typing import List, Optional
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from ocrmypdf.exceptions import (
|
||||
MissingDependencyError,
|
||||
SubprocessOutputError,
|
||||
TesseractConfigError,
|
||||
)
|
||||
from ocrmypdf.subprocess import get_version, run
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
OrientationConfidence = namedtuple('OrientationConfidence', ('angle', 'confidence'))
|
||||
|
||||
HOCR_TEMPLATE = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<title></title>
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
|
||||
<meta name='ocr-system' content='tesseract 4.0.0' />
|
||||
<meta name='ocr-capabilities' content='ocr_page ocr_carea ocr_par ocr_line ocrx_word ocrp_wconf'/>
|
||||
</head>
|
||||
<body>
|
||||
<div class='ocr_page' id='page_1' title='image "_blank.png"; bbox 0 0 {0} {1}; ppageno 0'>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
class TesseractLoggerAdapter(logging.LoggerAdapter):
|
||||
def process(self, msg, kwargs):
|
||||
kwargs['extra'] = self.extra
|
||||
return '[tesseract] %s' % (msg), kwargs
|
||||
|
||||
|
||||
class TesseractVersion(StrictVersion):
|
||||
|
||||
version_re = re.compile(
|
||||
r'''
|
||||
^(\d+) \. (\d+) (\. (\d+))? # groups: 1/major, 2/minor, 3/[skip], 4/patch
|
||||
[-]? # optional hyphen separator
|
||||
(?:(alpha|beta|rc|dev)?[.\-\ ]?(\d+)?)? # 5/prerelease, 6/prerelease_num
|
||||
(?:-(\d+)-g[0-9a-f]+)? # untagged git version
|
||||
$
|
||||
''',
|
||||
re.VERBOSE | re.ASCII,
|
||||
)
|
||||
|
||||
def parse(self, vstring):
|
||||
try:
|
||||
super().parse(vstring)
|
||||
except TypeError as e:
|
||||
if 'int() argument must be a string' in str(e):
|
||||
super().parse(vstring + '0')
|
||||
|
||||
|
||||
def version():
|
||||
return get_version('tesseract', regex=r'tesseract\s(.+)')
|
||||
|
||||
|
||||
def has_user_words():
|
||||
"""Does Tesseract have --user-words capability?
|
||||
|
||||
Not available in 4.0, but available in 4.1. Also available in 3.x, but
|
||||
we no longer support 3.x.
|
||||
"""
|
||||
return version() >= '4.1'
|
||||
|
||||
|
||||
def get_languages():
|
||||
def lang_error(output):
|
||||
msg = (
|
||||
"Tesseract failed to report available languages.\n"
|
||||
"Output from Tesseract:\n"
|
||||
"-----------\n"
|
||||
)
|
||||
msg += output
|
||||
return msg
|
||||
|
||||
args_tess = ['tesseract', '--list-langs']
|
||||
try:
|
||||
proc = run(
|
||||
args_tess,
|
||||
text=True,
|
||||
stdout=PIPE,
|
||||
stderr=STDOUT,
|
||||
logs_errors_to_stdout=True,
|
||||
check=True,
|
||||
)
|
||||
output = proc.stdout
|
||||
except CalledProcessError as e:
|
||||
raise MissingDependencyError(lang_error(e.output)) from e
|
||||
|
||||
for line in output.splitlines():
|
||||
if line.startswith('Error'):
|
||||
raise MissingDependencyError(lang_error(output))
|
||||
_header, *rest = output.splitlines()
|
||||
return {lang.strip() for lang in rest}
|
||||
|
||||
|
||||
def tess_base_args(langs: List[str], engine_mode: Optional[int]) -> List[str]:
|
||||
args = ['tesseract']
|
||||
if langs:
|
||||
args.extend(['-l', '+'.join(langs)])
|
||||
if engine_mode is not None:
|
||||
args.extend(['--oem', str(engine_mode)])
|
||||
return args
|
||||
|
||||
|
||||
def get_orientation(input_file: Path, engine_mode: Optional[int], timeout: float):
|
||||
args_tesseract = tess_base_args(['osd'], engine_mode) + [
|
||||
'--psm',
|
||||
'0',
|
||||
fspath(input_file),
|
||||
'stdout',
|
||||
]
|
||||
|
||||
try:
|
||||
p = run(args_tesseract, stdout=PIPE, stderr=STDOUT, timeout=timeout, check=True)
|
||||
stdout = p.stdout
|
||||
except TimeoutExpired:
|
||||
return OrientationConfidence(angle=0, confidence=0.0)
|
||||
except CalledProcessError as e:
|
||||
tesseract_log_output(e.stdout)
|
||||
tesseract_log_output(e.stderr)
|
||||
if (
|
||||
b'Too few characters. Skipping this page' in e.output
|
||||
or b'Image too large' in e.output
|
||||
):
|
||||
return OrientationConfidence(0, 0)
|
||||
raise SubprocessOutputError() from e
|
||||
else:
|
||||
osd = {}
|
||||
for line in stdout.decode().splitlines():
|
||||
line = line.strip()
|
||||
parts = line.split(':', maxsplit=2)
|
||||
if len(parts) == 2:
|
||||
osd[parts[0].strip()] = parts[1].strip()
|
||||
|
||||
angle = int(osd.get('Orientation in degrees', 0))
|
||||
oc = OrientationConfidence(
|
||||
angle=angle, confidence=float(osd.get('Orientation confidence', 0))
|
||||
)
|
||||
return oc
|
||||
|
||||
|
||||
def tesseract_log_output(stream):
|
||||
tlog = TesseractLoggerAdapter(
|
||||
log, extra=log.extra if hasattr(log, 'extra') else None
|
||||
)
|
||||
|
||||
if not stream:
|
||||
return
|
||||
try:
|
||||
text = stream.decode()
|
||||
except UnicodeDecodeError:
|
||||
text = stream.decode('utf-8', 'ignore')
|
||||
|
||||
lines = text.splitlines()
|
||||
for line in lines:
|
||||
if line.startswith("Tesseract Open Source"):
|
||||
continue
|
||||
elif line.startswith("Warning in pixReadMem"):
|
||||
continue
|
||||
elif 'diacritics' in line:
|
||||
tlog.warning("lots of diacritics - possibly poor OCR")
|
||||
elif line.startswith('OSD: Weak margin'):
|
||||
tlog.warning("unsure about page orientation")
|
||||
elif 'Error in pixScanForForeground' in line:
|
||||
pass # Appears to be spurious/problem with nonwhite borders
|
||||
elif 'Error in boxClipToRectangle' in line:
|
||||
pass # Always appears with pixScanForForeground message
|
||||
elif 'parameter not found: ' in line.lower():
|
||||
tlog.error(line.strip())
|
||||
problem = line.split('found: ')[1]
|
||||
raise TesseractConfigError(problem)
|
||||
elif 'error' in line.lower() or 'exception' in line.lower():
|
||||
tlog.error(line.strip())
|
||||
elif 'warning' in line.lower():
|
||||
tlog.warning(line.strip())
|
||||
elif 'read_params_file' in line.lower():
|
||||
tlog.error(line.strip())
|
||||
else:
|
||||
tlog.info(line.strip())
|
||||
|
||||
|
||||
def page_timedout(timeout):
|
||||
if timeout == 0:
|
||||
return
|
||||
log.warning("[tesseract] took too long to OCR - skipping")
|
||||
|
||||
|
||||
def _generate_null_hocr(output_hocr, output_text, image):
|
||||
"""Produce a .hocr file that reports no text detected on a page that is
|
||||
the same size as the input image."""
|
||||
with Image.open(image) as im:
|
||||
w, h = im.size
|
||||
|
||||
output_hocr.write_text(HOCR_TEMPLATE.format(w, h), encoding='utf-8')
|
||||
output_text.write_text('[skipped page]', encoding='utf-8')
|
||||
|
||||
|
||||
def generate_hocr(
|
||||
*,
|
||||
input_file: Path,
|
||||
output_hocr: Path,
|
||||
output_text: Path,
|
||||
languages: List[str],
|
||||
engine_mode: int,
|
||||
tessconfig: List[str],
|
||||
timeout: float,
|
||||
pagesegmode: int,
|
||||
user_words,
|
||||
user_patterns,
|
||||
):
|
||||
prefix = output_hocr.with_suffix('')
|
||||
|
||||
args_tesseract = tess_base_args(languages, engine_mode)
|
||||
|
||||
if pagesegmode is not None:
|
||||
args_tesseract.extend(['--psm', str(pagesegmode)])
|
||||
|
||||
if user_words:
|
||||
args_tesseract.extend(['--user-words', user_words])
|
||||
|
||||
if user_patterns:
|
||||
args_tesseract.extend(['--user-patterns', user_patterns])
|
||||
|
||||
# Reminder: test suite tesseract test plugins will break after any changes
|
||||
# to the number of order parameters here
|
||||
args_tesseract.extend([os.fspath(input_file), os.fspath(prefix), 'hocr', 'txt'])
|
||||
args_tesseract.extend(tessconfig)
|
||||
try:
|
||||
p = run(args_tesseract, stdout=PIPE, stderr=STDOUT, timeout=timeout, check=True)
|
||||
stdout = p.stdout
|
||||
except TimeoutExpired:
|
||||
# Generate a HOCR file with no recognized text if tesseract times out
|
||||
# Temporary workaround to hocrTransform not being able to function if
|
||||
# it does not have a valid hOCR file.
|
||||
page_timedout(timeout)
|
||||
_generate_null_hocr(output_hocr, output_text, input_file)
|
||||
except CalledProcessError as e:
|
||||
tesseract_log_output(e.output)
|
||||
if b'Image too large' in e.output:
|
||||
_generate_null_hocr(output_hocr, output_text, input_file)
|
||||
return
|
||||
|
||||
raise SubprocessOutputError() from e
|
||||
else:
|
||||
tesseract_log_output(stdout)
|
||||
# The sidecar text file will get the suffix .txt; rename it to
|
||||
# whatever caller wants it named
|
||||
if prefix.with_suffix('.txt').exists():
|
||||
shutil.move(prefix.with_suffix('.txt'), output_text)
|
||||
|
||||
|
||||
def use_skip_page(output_pdf, output_text):
|
||||
output_text.write_text('[skipped page]', encoding='utf-8')
|
||||
|
||||
# A 0 byte file to the output to indicate a skip
|
||||
output_pdf.write_bytes(b'')
|
||||
|
||||
|
||||
def generate_pdf(
|
||||
*,
|
||||
input_file: Path,
|
||||
output_pdf: Path,
|
||||
output_text: Path,
|
||||
languages: List[str],
|
||||
engine_mode: int,
|
||||
tessconfig: List[str],
|
||||
timeout: float,
|
||||
pagesegmode: int,
|
||||
user_words,
|
||||
user_patterns,
|
||||
):
|
||||
"""Use Tesseract to render a PDF.
|
||||
|
||||
input_file -- image to analyze
|
||||
output_pdf -- file to generate
|
||||
output_text -- OCR text file
|
||||
languages -- list of languages to consider
|
||||
engine_mode -- engine mode argument for tess v4
|
||||
tessconfig -- tesseract configuration
|
||||
timeout -- timeout (seconds)
|
||||
"""
|
||||
|
||||
args_tesseract = tess_base_args(languages, engine_mode)
|
||||
|
||||
if pagesegmode is not None:
|
||||
args_tesseract.extend(['--psm', str(pagesegmode)])
|
||||
|
||||
args_tesseract.extend(['-c', 'textonly_pdf=1'])
|
||||
|
||||
if user_words:
|
||||
args_tesseract.extend(['--user-words', user_words])
|
||||
|
||||
if user_patterns:
|
||||
args_tesseract.extend(['--user-patterns', user_patterns])
|
||||
|
||||
prefix = os.path.splitext(output_pdf)[0] # Tesseract appends suffixes
|
||||
|
||||
# Reminder: test suite tesseract test plugins might break after any changes
|
||||
# to the number of order parameters here
|
||||
|
||||
args_tesseract.extend([os.fspath(input_file), os.fspath(prefix), 'pdf', 'txt'])
|
||||
args_tesseract.extend(tessconfig)
|
||||
try:
|
||||
p = run(args_tesseract, stdout=PIPE, stderr=STDOUT, timeout=timeout, check=True)
|
||||
stdout = p.stdout
|
||||
if os.path.exists(prefix + '.txt'):
|
||||
shutil.move(prefix + '.txt', output_text)
|
||||
except TimeoutExpired:
|
||||
page_timedout(timeout)
|
||||
use_skip_page(output_pdf, output_text)
|
||||
except CalledProcessError as e:
|
||||
tesseract_log_output(e.output)
|
||||
if b'Image too large' in e.output:
|
||||
use_skip_page(output_pdf, output_text)
|
||||
return
|
||||
raise SubprocessOutputError() from e
|
||||
else:
|
||||
tesseract_log_output(stdout)
|
||||
@@ -0,0 +1,134 @@
|
||||
# © 2015 James R. Barlow: github.com/jbarlow83
|
||||
#
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
# unpaper documentation:
|
||||
# https://github.com/Flameeyes/unpaper/blob/master/doc/basic-concepts.md
|
||||
|
||||
"""Interface to unpaper executable"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import shlex
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
from subprocess import PIPE, STDOUT
|
||||
from tempfile import TemporaryDirectory
|
||||
from typing import List, Optional, Tuple, Union
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from ocrmypdf.exceptions import MissingDependencyError, SubprocessOutputError
|
||||
from ocrmypdf.subprocess import get_version
|
||||
from ocrmypdf.subprocess import run as external_run
|
||||
|
||||
DecFloat = Union[Decimal, float]
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def version() -> str:
|
||||
return get_version('unpaper')
|
||||
|
||||
|
||||
def _setup_unpaper_io(tmpdir: Path, input_file: Path) -> Tuple[Path, Path]:
|
||||
SUFFIXES = {'1': '.pbm', 'L': '.pgm', 'RGB': '.ppm'}
|
||||
with Image.open(input_file) as im:
|
||||
im_modified = False
|
||||
if im.mode not in SUFFIXES:
|
||||
log.info("Converting image to other colorspace")
|
||||
try:
|
||||
if im.mode == 'P' and len(im.getcolors()) == 2:
|
||||
im = im.convert(mode='1')
|
||||
else:
|
||||
im = im.convert(mode='RGB')
|
||||
except OSError as e:
|
||||
raise MissingDependencyError(
|
||||
"Could not convert image with type " + im.mode
|
||||
) from e
|
||||
else:
|
||||
im_modified = True
|
||||
try:
|
||||
suffix = SUFFIXES[im.mode]
|
||||
except KeyError:
|
||||
raise MissingDependencyError(
|
||||
"Failed to convert image to a supported format."
|
||||
) from None
|
||||
|
||||
if im_modified or input_file.suffix != '.pnm':
|
||||
input_pnm = tmpdir / 'input.pnm'
|
||||
im.save(input_pnm, format='PPM')
|
||||
else:
|
||||
# No changes, PNG input, just use the file we already have
|
||||
input_pnm = input_file
|
||||
output_pnm = tmpdir / f'output{suffix}'
|
||||
return input_pnm, output_pnm
|
||||
|
||||
|
||||
def run(
|
||||
input_file: Path, output_file: Path, *, dpi: DecFloat, mode_args: List[str]
|
||||
) -> None:
|
||||
args_unpaper = ['unpaper', '-v', '--dpi', str(round(dpi, 6))] + mode_args
|
||||
|
||||
with TemporaryDirectory() as tmpdir:
|
||||
input_pnm, output_pnm = _setup_unpaper_io(Path(tmpdir), input_file)
|
||||
|
||||
# To prevent any shenanigans from accepting arbitrary parameters in
|
||||
# --unpaper-args, we:
|
||||
# 1) run with cwd set to a tmpdir with only unpaper's files
|
||||
# 2) forbid the use of '/' in arguments, to prevent changing paths
|
||||
# 3) append absolute paths for the input and output file
|
||||
# This should ensure that a user cannot clobber some other file with
|
||||
# their unpaper arguments (whether intentionally or otherwise)
|
||||
args_unpaper.extend([os.fspath(input_pnm), os.fspath(output_pnm)])
|
||||
external_run(
|
||||
args_unpaper,
|
||||
close_fds=True,
|
||||
check=True,
|
||||
stderr=STDOUT, # unpaper writes logging output to stdout and stderr
|
||||
stdout=PIPE, # and cannot send file output to stdout
|
||||
cwd=tmpdir,
|
||||
logs_errors_to_stdout=True,
|
||||
)
|
||||
try:
|
||||
with Image.open(output_pnm) as imout:
|
||||
imout.save(output_file, dpi=(dpi, dpi))
|
||||
except OSError as e:
|
||||
raise SubprocessOutputError(
|
||||
"unpaper: failed to produce the expected output file. "
|
||||
+ " Called with: "
|
||||
+ str(args_unpaper)
|
||||
) from e
|
||||
|
||||
|
||||
def validate_custom_args(args: str) -> List[str]:
|
||||
unpaper_args = shlex.split(args)
|
||||
if any(('/' in arg or arg == '.' or arg == '..') for arg in unpaper_args):
|
||||
raise ValueError('No filenames allowed in --unpaper-args')
|
||||
return unpaper_args
|
||||
|
||||
|
||||
def clean(
|
||||
input_file: Path,
|
||||
output_file: Path,
|
||||
*,
|
||||
dpi: DecFloat,
|
||||
unpaper_args: Optional[List[str]] = None,
|
||||
):
|
||||
default_args = [
|
||||
'--layout',
|
||||
'none',
|
||||
'--mask-scan-size',
|
||||
'100', # don't blank out narrow columns
|
||||
'--no-border-align', # don't align visible content to borders
|
||||
'--no-mask-center', # don't center visible content within page
|
||||
'--no-grayfilter', # don't remove light gray areas
|
||||
'--no-blackfilter', # don't remove solid black areas
|
||||
'--no-deskew', # don't deskew
|
||||
]
|
||||
if not unpaper_args:
|
||||
unpaper_args = default_args
|
||||
run(input_file, output_file, dpi=dpi, mode_args=unpaper_args)
|
||||
@@ -0,0 +1,317 @@
|
||||
# © 2018 James R. Barlow: github.com/jbarlow83
|
||||
#
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
import logging
|
||||
import uuid
|
||||
from contextlib import suppress
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from pikepdf import (
|
||||
Dictionary,
|
||||
Name,
|
||||
Object,
|
||||
Operator,
|
||||
Page,
|
||||
Pdf,
|
||||
PdfError,
|
||||
PdfMatrix,
|
||||
Stream,
|
||||
parse_content_stream,
|
||||
unparse_content_stream,
|
||||
)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
MAX_REPLACE_PAGES = 100
|
||||
|
||||
|
||||
def _ensure_dictionary(obj, name):
|
||||
if name not in obj:
|
||||
obj[name] = Dictionary({})
|
||||
return obj[name]
|
||||
|
||||
|
||||
def _update_resources(*, obj, font, font_key, procset):
|
||||
"""Update this obj's fonts with a reference to the Glyphless font.
|
||||
|
||||
obj can be a page or Form XObject.
|
||||
"""
|
||||
|
||||
resources = _ensure_dictionary(obj, Name.Resources)
|
||||
fonts = _ensure_dictionary(resources, Name.Font)
|
||||
if font_key is not None and font_key not in fonts:
|
||||
fonts[font_key] = font
|
||||
|
||||
# Reassign /ProcSet to one that just lists everything - ProcSet is
|
||||
# obsolete and doesn't matter but recommended for old viewer support
|
||||
if procset:
|
||||
resources['/ProcSet'] = procset
|
||||
|
||||
|
||||
def strip_invisible_text(pdf, page):
|
||||
stream = []
|
||||
in_text_obj = False
|
||||
render_mode = 0
|
||||
text_objects = []
|
||||
|
||||
rich_page = Page(page)
|
||||
rich_page.contents_coalesce()
|
||||
for operands, operator in parse_content_stream(page, ''):
|
||||
if not in_text_obj:
|
||||
if operator == Operator('BT'):
|
||||
in_text_obj = True
|
||||
render_mode = 0
|
||||
text_objects.append((operands, operator))
|
||||
else:
|
||||
stream.append((operands, operator))
|
||||
else:
|
||||
if operator == Operator('Tr'):
|
||||
render_mode = operands[0]
|
||||
text_objects.append((operands, operator))
|
||||
if operator == Operator('ET'):
|
||||
in_text_obj = False
|
||||
if render_mode != 3:
|
||||
stream.extend(text_objects)
|
||||
text_objects.clear()
|
||||
|
||||
content_stream = unparse_content_stream(stream)
|
||||
page.Contents = Stream(pdf, content_stream)
|
||||
|
||||
|
||||
class OcrGrafter:
|
||||
def __init__(self, context):
|
||||
self.context = context
|
||||
self.path_base = context.origin
|
||||
|
||||
self.pdf_base = Pdf.open(self.path_base)
|
||||
self.font, self.font_key = None, None
|
||||
|
||||
self.pdfinfo = context.pdfinfo
|
||||
self.output_file = context.get_path('graft_layers.pdf')
|
||||
|
||||
self.procset = self.pdf_base.make_indirect(
|
||||
Object.parse(b'[ /PDF /Text /ImageB /ImageC /ImageI ]')
|
||||
)
|
||||
|
||||
self.emplacements = 1
|
||||
self.interim_count = 0
|
||||
|
||||
def graft_page(
|
||||
self,
|
||||
*,
|
||||
pageno: int,
|
||||
image: Optional[Path],
|
||||
textpdf: Optional[Path],
|
||||
autorotate_correction: int,
|
||||
):
|
||||
if textpdf and not self.font:
|
||||
self.font, self.font_key = self._find_font(textpdf)
|
||||
|
||||
emplaced_page = False
|
||||
content_rotation = self.pdfinfo[pageno].rotation
|
||||
path_image = Path(image).resolve() if image else None
|
||||
if path_image is not None and path_image != self.path_base:
|
||||
# We are updating the old page with a rasterized PDF of the new
|
||||
# page (without changing objgen, to preserve references)
|
||||
log.debug("Emplacement update")
|
||||
with Pdf.open(path_image) as pdf_image:
|
||||
self.emplacements += 1
|
||||
foreign_image_page = pdf_image.pages[0]
|
||||
self.pdf_base.pages.append(foreign_image_page)
|
||||
local_image_page = self.pdf_base.pages[-1]
|
||||
self.pdf_base.pages[pageno].emplace(local_image_page)
|
||||
del self.pdf_base.pages[-1]
|
||||
emplaced_page = True
|
||||
|
||||
# Calculate if the text is misaligned compared to the content
|
||||
if emplaced_page:
|
||||
content_rotation = autorotate_correction
|
||||
text_rotation = autorotate_correction
|
||||
text_misaligned = (text_rotation - content_rotation) % 360
|
||||
log.debug(
|
||||
f"Text rotation: (text, autorotate, content) -> text misalignment = "
|
||||
f"({text_rotation}, {autorotate_correction}, {content_rotation}) -> {text_misaligned}"
|
||||
)
|
||||
|
||||
if textpdf and self.font:
|
||||
# Graft the text layer onto this page, whether new or old, possibly
|
||||
# rotating the text layer by the amount is misaligned.
|
||||
strip_old = self.context.options.redo_ocr
|
||||
self._graft_text_layer(
|
||||
page_num=pageno + 1,
|
||||
textpdf=textpdf,
|
||||
font=self.font,
|
||||
font_key=self.font_key,
|
||||
text_rotation=text_misaligned,
|
||||
procset=self.procset,
|
||||
strip_old_text=strip_old,
|
||||
)
|
||||
|
||||
# Correct the overall page rotation if needed, now that the text and content
|
||||
# are aligned
|
||||
page_rotation = (content_rotation - autorotate_correction) % 360
|
||||
self.pdf_base.pages[pageno].Rotate = page_rotation
|
||||
log.debug(
|
||||
f"Page rotation: (content, auto) -> page = "
|
||||
f"({content_rotation}, {autorotate_correction}) -> {page_rotation}"
|
||||
)
|
||||
if self.emplacements % MAX_REPLACE_PAGES == 0:
|
||||
self.save_and_reload()
|
||||
|
||||
def save_and_reload(self):
|
||||
"""Save and reload the Pdf.
|
||||
|
||||
This will keep a lid on our memory usage for very large files. Attach
|
||||
the font to page 1 even if page 1 doesn't use it, so we have a way to get it
|
||||
back.
|
||||
"""
|
||||
|
||||
page0 = self.pdf_base.pages[0]
|
||||
_update_resources(
|
||||
obj=page0, font=self.font, font_key=self.font_key, procset=self.procset
|
||||
)
|
||||
|
||||
# We cannot read and write the same file, that will corrupt it
|
||||
# but we don't to keep more copies than we need to. Delete intermediates.
|
||||
# {interim_count} is the opened file we were updating
|
||||
# {interim_count - 1} can be deleted
|
||||
# {interim_count + 1} is the new file will produce and open
|
||||
old_file = self.output_file.with_suffix(f'.working{self.interim_count - 1}.pdf')
|
||||
if not self.context.options.keep_temporary_files:
|
||||
with suppress(FileNotFoundError):
|
||||
old_file.unlink()
|
||||
|
||||
next_file = self.output_file.with_suffix(
|
||||
f'.working{self.interim_count + 1}.pdf'
|
||||
)
|
||||
self.pdf_base.save(next_file)
|
||||
self.pdf_base.close()
|
||||
|
||||
self.pdf_base = Pdf.open(next_file)
|
||||
self.procset = self.pdf_base.pages[0].Resources.ProcSet
|
||||
self.font, self.font_key = None, None # Ensure we reacquire this information
|
||||
self.interim_count += 1
|
||||
|
||||
def finalize(self):
|
||||
self.pdf_base.save(self.output_file)
|
||||
self.pdf_base.close()
|
||||
return self.output_file
|
||||
|
||||
def _find_font(self, text):
|
||||
"""Copy a font from the filename text into pdf_base"""
|
||||
|
||||
font, font_key = None, None
|
||||
possible_font_names = ('/f-0-0', '/F1')
|
||||
try:
|
||||
with Pdf.open(text) as pdf_text:
|
||||
try:
|
||||
pdf_text_fonts = pdf_text.pages[0].Resources.get('/Font', {})
|
||||
except (AttributeError, IndexError, KeyError):
|
||||
return None, None
|
||||
pdf_text_font = None
|
||||
for f in possible_font_names:
|
||||
pdf_text_font = pdf_text_fonts.get(f, None)
|
||||
if pdf_text_font is not None:
|
||||
font_key = f
|
||||
break
|
||||
if pdf_text_font:
|
||||
font = self.pdf_base.copy_foreign(pdf_text_font)
|
||||
return font, font_key
|
||||
except (FileNotFoundError, PdfError):
|
||||
# PdfError occurs if a 0-length file is written e.g. due to OCR timeout
|
||||
return None, None
|
||||
|
||||
def _graft_text_layer(
|
||||
self,
|
||||
*,
|
||||
page_num: int,
|
||||
textpdf: Path,
|
||||
font: Object,
|
||||
font_key: Object,
|
||||
procset: Object,
|
||||
text_rotation: int,
|
||||
strip_old_text: bool,
|
||||
):
|
||||
"""Insert the text layer from text page 0 on to pdf_base at page_num"""
|
||||
|
||||
log.debug("Grafting")
|
||||
if Path(textpdf).stat().st_size == 0:
|
||||
return
|
||||
|
||||
# This is a pointer indicating a specific page in the base file
|
||||
with Pdf.open(textpdf) as pdf_text:
|
||||
pdf_text_contents = pdf_text.pages[0].Contents.read_bytes()
|
||||
|
||||
base_page = self.pdf_base.pages.p(page_num)
|
||||
|
||||
# The text page always will be oriented up by this stage but the original
|
||||
# content may have a rotation applied. Wrap the text stream with a rotation
|
||||
# so it will be oriented the same way as the rest of the page content.
|
||||
# (Previous versions OCRmyPDF rotated the content layer to match the text.)
|
||||
mediabox = [float(pdf_text.pages[0].MediaBox[v]) for v in range(4)]
|
||||
wt, ht = mediabox[2] - mediabox[0], mediabox[3] - mediabox[1]
|
||||
|
||||
mediabox = [float(base_page.MediaBox[v]) for v in range(4)]
|
||||
wp, hp = mediabox[2] - mediabox[0], mediabox[3] - mediabox[1]
|
||||
|
||||
translate = PdfMatrix().translated(-wt / 2, -ht / 2)
|
||||
untranslate = PdfMatrix().translated(wp / 2, hp / 2)
|
||||
corner = PdfMatrix().translated(mediabox[0], mediabox[1])
|
||||
# -rotation because the input is a clockwise angle and this formula
|
||||
# uses CCW
|
||||
text_rotation = -text_rotation % 360
|
||||
rotate = PdfMatrix().rotated(text_rotation)
|
||||
|
||||
# Because of rounding of DPI, we might get a text layer that is not
|
||||
# identically sized to the target page. Scale to adjust. Normally this
|
||||
# is within 0.998.
|
||||
if text_rotation in (90, 270):
|
||||
wt, ht = ht, wt
|
||||
scale_x = wp / wt
|
||||
scale_y = hp / ht
|
||||
|
||||
# log.debug('%r', scale_x, scale_y)
|
||||
scale = PdfMatrix().scaled(scale_x, scale_y)
|
||||
|
||||
# Translate the text so it is centered at (0, 0), rotate it there, adjust
|
||||
# for a size different between initial and text PDF, then untranslate, and
|
||||
# finally move the lower left corner to match the mediabox
|
||||
ctm = translate @ rotate @ scale @ untranslate @ corner
|
||||
|
||||
base_resources = _ensure_dictionary(base_page, Name.Resources)
|
||||
base_xobjs = _ensure_dictionary(base_resources, Name.XObject)
|
||||
text_xobj_name = Name('/' + str(uuid.uuid4()))
|
||||
xobj = self.pdf_base.make_stream(pdf_text_contents)
|
||||
base_xobjs[text_xobj_name] = xobj
|
||||
xobj.Type = Name.XObject
|
||||
xobj.Subtype = Name.Form
|
||||
xobj.FormType = 1
|
||||
xobj.BBox = mediabox
|
||||
_update_resources(
|
||||
obj=xobj, font=font, font_key=font_key, procset=[Name.PDF]
|
||||
)
|
||||
|
||||
pdf_draw_xobj = (
|
||||
(b'q %s cm\n' % ctm.encode()) + (b'%s Do\n' % text_xobj_name) + b'\nQ\n'
|
||||
)
|
||||
new_text_layer = Stream(self.pdf_base, pdf_draw_xobj)
|
||||
|
||||
if strip_old_text:
|
||||
strip_invisible_text(self.pdf_base, base_page)
|
||||
|
||||
if hasattr(Page, 'contents_add'):
|
||||
# pikepdf >= 2.14 adds this method and deprecates the one below
|
||||
Page(base_page).contents_add(new_text_layer, prepend=True)
|
||||
else:
|
||||
# pikepdf < 2.14
|
||||
base_page.page_contents_add(
|
||||
new_text_layer, prepend=True
|
||||
) # pragma: no cover
|
||||
|
||||
_update_resources(
|
||||
obj=base_page, font=font, font_key=font_key, procset=procset
|
||||
)
|
||||
@@ -0,0 +1,103 @@
|
||||
# © 2018 James R. Barlow: github.com/jbarlow83
|
||||
#
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
from argparse import Namespace
|
||||
from copy import copy
|
||||
from pathlib import Path
|
||||
from typing import Iterator
|
||||
|
||||
from pluggy import PluginManager
|
||||
|
||||
from ocrmypdf.pdfinfo import PdfInfo
|
||||
from ocrmypdf.pdfinfo.info import PageInfo
|
||||
|
||||
|
||||
class PdfContext:
|
||||
"""Holds the context for a particular run of the pipeline."""
|
||||
|
||||
options: Namespace #: The specified options for processing this PDF.
|
||||
origin: Path #: The filename of the original input file.
|
||||
pdfinfo: PdfInfo #: Detailed data for this PDF.
|
||||
plugin_manager: PluginManager #: PluginManager for processing the current PDF.
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
options: Namespace,
|
||||
work_folder: Path,
|
||||
origin: Path,
|
||||
pdfinfo: PdfInfo,
|
||||
plugin_manager,
|
||||
):
|
||||
self.options = options
|
||||
self.work_folder = work_folder
|
||||
self.origin = origin
|
||||
self.pdfinfo = pdfinfo
|
||||
self.plugin_manager = plugin_manager
|
||||
|
||||
def get_path(self, name: str) -> Path:
|
||||
"""Generate a ``Path`` for an intermediate file involved in processing.
|
||||
|
||||
The path will be in a temporary folder that is common for all processing
|
||||
of this particular PDF.
|
||||
"""
|
||||
return self.work_folder / name
|
||||
|
||||
def get_page_contexts(self) -> Iterator['PageContext']:
|
||||
"""Get all ``PageContext`` for this PDF."""
|
||||
npages = len(self.pdfinfo)
|
||||
for n in range(npages):
|
||||
yield PageContext(self, n)
|
||||
|
||||
|
||||
class PageContext:
|
||||
"""Holds our context for a page.
|
||||
|
||||
Must be pickable, so stores only intrinsic/simple data elements or those
|
||||
capable of their serializing themselves via ``__getstate__``.
|
||||
"""
|
||||
|
||||
options: Namespace #: The specified options for processing this PDF.
|
||||
origin: Path #: The filename of the original input file.
|
||||
pageno: int #: This page number (zero-based).
|
||||
pageinfo: PageInfo #: Information on this page.
|
||||
plugin_manager: PluginManager #: PluginManager for processing the current PDF.
|
||||
|
||||
def __init__(self, pdf_context: PdfContext, pageno):
|
||||
self.work_folder = pdf_context.work_folder
|
||||
self.origin = pdf_context.origin
|
||||
self.options = pdf_context.options
|
||||
self.pageno = pageno
|
||||
self.pageinfo = pdf_context.pdfinfo[pageno]
|
||||
self.plugin_manager = pdf_context.plugin_manager
|
||||
|
||||
def get_path(self, name: str) -> Path:
|
||||
"""Generate a ``Path`` for a file that is part of processing this page.
|
||||
|
||||
The path will be based in a common temporary folder and have a prefix based
|
||||
on the page number.
|
||||
"""
|
||||
return self.work_folder / ("%06d_%s" % (self.pageno + 1, name))
|
||||
|
||||
def __getstate__(self):
|
||||
state = self.__dict__.copy()
|
||||
|
||||
state['options'] = copy(self.options)
|
||||
if not isinstance(state['options'].input_file, (str, bytes, os.PathLike)):
|
||||
state['options'].input_file = 'stream'
|
||||
if not isinstance(state['options'].output_file, (str, bytes, os.PathLike)):
|
||||
state['options'].output_file = 'stream'
|
||||
return state
|
||||
|
||||
|
||||
def cleanup_working_files(work_folder: Path, options: Namespace):
|
||||
if options.keep_temporary_files:
|
||||
print(f"Temporary working files retained at:\n{work_folder}", file=sys.stderr)
|
||||
else:
|
||||
shutil.rmtree(work_folder, ignore_errors=True)
|
||||
@@ -0,0 +1,50 @@
|
||||
# © 2020 James R. Barlow: github.com/jbarlow83
|
||||
#
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
import logging
|
||||
import sys
|
||||
from contextlib import suppress
|
||||
|
||||
from tqdm import tqdm
|
||||
|
||||
|
||||
class PageNumberFilter(logging.Filter):
|
||||
def filter(self, record):
|
||||
pageno = getattr(record, 'pageno', None)
|
||||
if isinstance(pageno, int):
|
||||
record.pageno = f'{pageno:5d} '
|
||||
elif pageno is None:
|
||||
record.pageno = ''
|
||||
return True
|
||||
|
||||
|
||||
class TqdmConsole:
|
||||
"""Wrapper to log messages in a way that is compatible with tqdm progress bar
|
||||
|
||||
This routes log messages through tqdm so that it can print them above the
|
||||
progress bar, and then refresh the progress bar, rather than overwriting
|
||||
it which looks messy.
|
||||
|
||||
For some reason Python 3.6 prints extra empty messages from time to time,
|
||||
so we suppress those.
|
||||
"""
|
||||
|
||||
def __init__(self, file):
|
||||
self.file = file
|
||||
self.py36 = sys.version_info[0:2] == (3, 6)
|
||||
|
||||
def write(self, msg):
|
||||
# When no progress bar is active, tqdm.write() routes to print()
|
||||
if self.py36:
|
||||
if msg.strip() != '':
|
||||
tqdm.write(msg.rstrip(), end='\n', file=self.file)
|
||||
else:
|
||||
tqdm.write(msg.rstrip(), end='\n', file=self.file)
|
||||
|
||||
def flush(self):
|
||||
with suppress(AttributeError):
|
||||
self.file.flush()
|
||||
@@ -0,0 +1,892 @@
|
||||
# © 2016 James R. Barlow: github.com/jbarlow83
|
||||
#
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from contextlib import suppress
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from shutil import copyfileobj
|
||||
from typing import Dict, Iterable, Optional
|
||||
|
||||
import img2pdf
|
||||
import pikepdf
|
||||
from pikepdf.models.metadata import encode_pdf_date
|
||||
from PIL import Image, ImageColor, ImageDraw
|
||||
|
||||
from ocrmypdf import leptonica
|
||||
from ocrmypdf._concurrent import Executor
|
||||
from ocrmypdf._exec import unpaper
|
||||
from ocrmypdf._jobcontext import PageContext, PdfContext
|
||||
from ocrmypdf._version import PROGRAM_NAME
|
||||
from ocrmypdf._version import __version__ as VERSION
|
||||
from ocrmypdf.exceptions import (
|
||||
DpiError,
|
||||
EncryptedPdfError,
|
||||
InputFileError,
|
||||
PriorOcrFoundError,
|
||||
UnsupportedImageFormatError,
|
||||
)
|
||||
from ocrmypdf.helpers import Resolution, safe_symlink
|
||||
from ocrmypdf.hocrtransform import HocrTransform
|
||||
from ocrmypdf.optimize import optimize
|
||||
from ocrmypdf.pdfa import generate_pdfa_ps
|
||||
from ocrmypdf.pdfinfo import Colorspace, Encoding, PdfInfo
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
VECTOR_PAGE_DPI = 400
|
||||
|
||||
|
||||
def triage_image_file(input_file, output_file, options):
|
||||
log.info("Input file is not a PDF, checking if it is an image...")
|
||||
try:
|
||||
im = Image.open(input_file)
|
||||
except OSError as e:
|
||||
# Recover the original filename
|
||||
log.error(str(e).replace(str(input_file), str(options.input_file)))
|
||||
raise UnsupportedImageFormatError() from e
|
||||
|
||||
with im:
|
||||
log.info("Input file is an image")
|
||||
if 'dpi' in im.info:
|
||||
if im.info['dpi'] <= (96, 96) and not options.image_dpi:
|
||||
log.info("Image size: (%d, %d)", *im.size)
|
||||
log.info("Image resolution: (%d, %d)", *im.info['dpi'])
|
||||
log.error(
|
||||
"Input file is an image, but the resolution (DPI) is "
|
||||
"not credible. Estimate the resolution at which the "
|
||||
"image was scanned and specify it using --image-dpi."
|
||||
)
|
||||
raise DpiError()
|
||||
elif not options.image_dpi:
|
||||
log.info("Image size: (%d, %d)", *im.size)
|
||||
log.error(
|
||||
"Input file is an image, but has no resolution (DPI) "
|
||||
"in its metadata. Estimate the resolution at which "
|
||||
"image was scanned and specify it using --image-dpi."
|
||||
)
|
||||
raise DpiError()
|
||||
|
||||
if im.mode in ('RGBA', 'LA'):
|
||||
log.error(
|
||||
"The input image has an alpha channel. Remove the alpha "
|
||||
"channel first."
|
||||
)
|
||||
raise UnsupportedImageFormatError()
|
||||
|
||||
if 'iccprofile' not in im.info:
|
||||
if im.mode == 'RGB':
|
||||
log.info("Input image has no ICC profile, assuming sRGB")
|
||||
elif im.mode == 'CMYK':
|
||||
log.error("Input CMYK image has no ICC profile, not usable")
|
||||
raise UnsupportedImageFormatError()
|
||||
|
||||
try:
|
||||
log.info("Image seems valid. Try converting to PDF...")
|
||||
layout_fun = img2pdf.default_layout_fun
|
||||
if options.image_dpi:
|
||||
layout_fun = img2pdf.get_fixed_dpi_layout_fun(
|
||||
Resolution(options.image_dpi, options.image_dpi)
|
||||
)
|
||||
with open(output_file, 'wb') as outf:
|
||||
img2pdf.convert(
|
||||
os.fspath(input_file),
|
||||
layout_fun=layout_fun,
|
||||
with_pdfrw=False,
|
||||
outputstream=outf,
|
||||
)
|
||||
log.info("Successfully converted to PDF, processing...")
|
||||
except img2pdf.ImageOpenError as e:
|
||||
log.error(e)
|
||||
raise UnsupportedImageFormatError() from e
|
||||
|
||||
|
||||
def _pdf_guess_version(input_file, search_window=1024):
|
||||
"""Try to find version signature at start of file.
|
||||
|
||||
Not robust enough to deal with appended files.
|
||||
|
||||
Returns empty string if not found, indicating file is probably not PDF.
|
||||
"""
|
||||
|
||||
with open(input_file, 'rb') as f:
|
||||
signature = f.read(search_window)
|
||||
m = re.search(br'%PDF-(\d\.\d)', signature)
|
||||
if m:
|
||||
return m.group(1)
|
||||
return ''
|
||||
|
||||
|
||||
def triage(original_filename, input_file, output_file, options):
|
||||
try:
|
||||
if _pdf_guess_version(input_file):
|
||||
if options.image_dpi:
|
||||
log.warning(
|
||||
"Argument --image-dpi is being ignored because the "
|
||||
"input file is a PDF, not an image."
|
||||
)
|
||||
# Origin file is a pdf create a symlink with pdf extension
|
||||
safe_symlink(input_file, output_file)
|
||||
return output_file
|
||||
except OSError as e:
|
||||
log.debug(f"Temporary file was at: {input_file}")
|
||||
msg = str(e).replace(str(input_file), original_filename)
|
||||
raise InputFileError(msg) from e
|
||||
|
||||
triage_image_file(input_file, output_file, options)
|
||||
return output_file
|
||||
|
||||
|
||||
def get_pdfinfo(
|
||||
input_file,
|
||||
*,
|
||||
executor: Executor,
|
||||
detailed_analysis=False,
|
||||
progbar=False,
|
||||
max_workers=None,
|
||||
check_pages=None,
|
||||
) -> PdfInfo:
|
||||
try:
|
||||
return PdfInfo(
|
||||
input_file,
|
||||
detailed_analysis=detailed_analysis,
|
||||
progbar=progbar,
|
||||
max_workers=max_workers,
|
||||
check_pages=check_pages,
|
||||
executor=executor,
|
||||
)
|
||||
except pikepdf.PasswordError as e:
|
||||
raise EncryptedPdfError() from e
|
||||
except pikepdf.PdfError as e:
|
||||
raise InputFileError() from e
|
||||
|
||||
|
||||
def validate_pdfinfo_options(context: PdfContext):
|
||||
pdfinfo = context.pdfinfo
|
||||
options = context.options
|
||||
|
||||
if pdfinfo.needs_rendering:
|
||||
log.error(
|
||||
"This PDF contains dynamic XFA forms created by Adobe LiveCycle "
|
||||
"Designer and can only be read by Adobe Acrobat or Adobe Reader."
|
||||
)
|
||||
raise InputFileError()
|
||||
if pdfinfo.has_userunit and options.output_type.startswith('pdfa'):
|
||||
log.error(
|
||||
"This input file uses a PDF feature that is not supported "
|
||||
"by Ghostscript, so you cannot use --output-type=pdfa for this "
|
||||
"file. (Specifically, it uses the PDF-1.6 /UserUnit feature to "
|
||||
"support very large or small page sizes, and Ghostscript cannot "
|
||||
"output these files.) Use --output-type=pdf instead."
|
||||
)
|
||||
raise InputFileError()
|
||||
if pdfinfo.has_acroform:
|
||||
if options.redo_ocr:
|
||||
log.error(
|
||||
"This PDF has a user fillable form. --redo-ocr is not "
|
||||
"currently possible on such files."
|
||||
)
|
||||
raise InputFileError()
|
||||
else:
|
||||
log.warning(
|
||||
"This PDF has a fillable form. "
|
||||
"Chances are it is a pure digital "
|
||||
"document that does not need OCR."
|
||||
)
|
||||
if not options.force_ocr:
|
||||
log.info(
|
||||
"Use the option --force-ocr to produce an image of the "
|
||||
"form and all filled form fields. The output PDF will be "
|
||||
"'flattened' and will no longer be fillable."
|
||||
)
|
||||
context.plugin_manager.hook.validate(pdfinfo=pdfinfo, options=options)
|
||||
|
||||
|
||||
def _vector_page_dpi(pageinfo):
|
||||
return VECTOR_PAGE_DPI if pageinfo.has_vector or pageinfo.has_text else 0.0
|
||||
|
||||
|
||||
def get_page_dpi(pageinfo, options):
|
||||
"Get the DPI when nonsquare DPI is tolerable"
|
||||
xres = max(
|
||||
pageinfo.dpi.x or VECTOR_PAGE_DPI,
|
||||
options.oversample or 0.0,
|
||||
_vector_page_dpi(pageinfo),
|
||||
)
|
||||
yres = max(
|
||||
pageinfo.dpi.y or VECTOR_PAGE_DPI,
|
||||
options.oversample or 0,
|
||||
_vector_page_dpi(pageinfo),
|
||||
)
|
||||
return Resolution(float(xres), float(yres))
|
||||
|
||||
|
||||
def get_page_square_dpi(pageinfo, options) -> Resolution:
|
||||
"Get the DPI when we require xres == yres, scaled to physical units"
|
||||
xres = pageinfo.dpi.x or 0.0
|
||||
yres = pageinfo.dpi.y or 0.0
|
||||
userunit = float(pageinfo.userunit) or 1.0
|
||||
units = float(
|
||||
max(
|
||||
(xres * userunit) or VECTOR_PAGE_DPI,
|
||||
(yres * userunit) or VECTOR_PAGE_DPI,
|
||||
_vector_page_dpi(pageinfo),
|
||||
options.oversample or 0.0,
|
||||
)
|
||||
)
|
||||
return Resolution(units, units)
|
||||
|
||||
|
||||
def get_canvas_square_dpi(pageinfo, options) -> Resolution:
|
||||
"""Get the DPI when we require xres == yres, in Postscript units"""
|
||||
units = float(
|
||||
max(
|
||||
(pageinfo.dpi.x) or VECTOR_PAGE_DPI,
|
||||
(pageinfo.dpi.y) or VECTOR_PAGE_DPI,
|
||||
_vector_page_dpi(pageinfo),
|
||||
options.oversample or 0.0,
|
||||
)
|
||||
)
|
||||
return Resolution(units, units)
|
||||
|
||||
|
||||
def is_ocr_required(page_context: PageContext):
|
||||
pageinfo = page_context.pageinfo
|
||||
options = page_context.options
|
||||
|
||||
ocr_required = True
|
||||
|
||||
if options.pages and pageinfo.pageno not in options.pages:
|
||||
log.debug(f"skipped {pageinfo.pageno} as requested by --pages {options.pages}")
|
||||
ocr_required = False
|
||||
elif pageinfo.has_text:
|
||||
if not options.force_ocr and not (options.skip_text or options.redo_ocr):
|
||||
raise PriorOcrFoundError(
|
||||
"page already has text! - aborting (use --force-ocr to force OCR; "
|
||||
" see also help for the arguments --skip-text and --redo-ocr"
|
||||
)
|
||||
elif options.force_ocr:
|
||||
log.info("page already has text! - rasterizing text and running OCR anyway")
|
||||
ocr_required = True
|
||||
elif options.redo_ocr:
|
||||
if pageinfo.has_corrupt_text:
|
||||
log.warning(
|
||||
"some text on this page cannot be mapped to characters: "
|
||||
"consider using --force-ocr instead"
|
||||
)
|
||||
else:
|
||||
log.info("redoing OCR")
|
||||
ocr_required = True
|
||||
elif options.skip_text:
|
||||
log.info("skipping all processing on this page")
|
||||
ocr_required = False
|
||||
elif not pageinfo.images and not options.lossless_reconstruction:
|
||||
# We found a page with no images and no text. That means it may
|
||||
# have vector art that the user wants to OCR. If we determined
|
||||
# lossless reconstruction is not possible then we have to rasterize
|
||||
# the image. So if OCR is being forced, take that to mean YES, go
|
||||
# ahead and rasterize. If not forced, then pretend there's no text
|
||||
# on the page at all so we don't lose anything.
|
||||
# This could be made smarter by explicitly searching for vector art.
|
||||
if options.force_ocr and options.oversample:
|
||||
# The user really wants to reprocess this file
|
||||
log.info(
|
||||
"page has no images - "
|
||||
f"rasterizing at {options.oversample} DPI because "
|
||||
"--force-ocr --oversample was specified"
|
||||
)
|
||||
elif options.force_ocr:
|
||||
# Warn the user they might not want to do this
|
||||
log.warning(
|
||||
"page has no images - "
|
||||
"all vector content will be "
|
||||
f"rasterized at {VECTOR_PAGE_DPI} DPI, losing some resolution and likely "
|
||||
"increasing file size. Use --oversample to adjust the "
|
||||
"DPI."
|
||||
)
|
||||
else:
|
||||
log.info(
|
||||
"page has no images - "
|
||||
"skipping all processing on this page to avoid losing detail. "
|
||||
"Use --force-ocr if you wish to perform OCR on pages that "
|
||||
"have vector content."
|
||||
)
|
||||
ocr_required = False
|
||||
|
||||
if ocr_required and options.skip_big and pageinfo.images:
|
||||
pixel_count = pageinfo.width_pixels * pageinfo.height_pixels
|
||||
if pixel_count > (options.skip_big * 1_000_000):
|
||||
ocr_required = False
|
||||
log.warning(
|
||||
"page too big, skipping OCR "
|
||||
f"({(pixel_count / 1_000_000):.1f} MPixels > {options.skip_big:.1f} MPixels --skip-big)"
|
||||
)
|
||||
return ocr_required
|
||||
|
||||
|
||||
def rasterize_preview(input_file: Path, page_context: PageContext):
|
||||
output_file = page_context.get_path('rasterize_preview.jpg')
|
||||
canvas_dpi = get_canvas_square_dpi(page_context.pageinfo, page_context.options)
|
||||
page_dpi = get_page_square_dpi(page_context.pageinfo, page_context.options)
|
||||
page_context.plugin_manager.hook.rasterize_pdf_page(
|
||||
input_file=input_file,
|
||||
output_file=output_file,
|
||||
raster_device='jpeggray',
|
||||
raster_dpi=canvas_dpi,
|
||||
pageno=page_context.pageinfo.pageno + 1,
|
||||
page_dpi=page_dpi,
|
||||
rotation=0,
|
||||
filter_vector=False,
|
||||
)
|
||||
return output_file
|
||||
|
||||
|
||||
def describe_rotation(page_context: PageContext, orient_conf, correction: int):
|
||||
"""
|
||||
Describe the page rotation we are going to perform.
|
||||
"""
|
||||
direction = {0: '⇧', 90: '⇨', 180: '⇩', 270: '⇦'}
|
||||
turns = {0: ' ', 90: '⬏', 180: '↻', 270: '⬑'}
|
||||
|
||||
existing_rotation = page_context.pageinfo.rotation
|
||||
action = ''
|
||||
if orient_conf.confidence >= page_context.options.rotate_pages_threshold:
|
||||
if correction != 0:
|
||||
action = 'will rotate ' + turns[correction]
|
||||
else:
|
||||
action = 'rotation appears correct'
|
||||
else:
|
||||
if correction != 0:
|
||||
action = 'confidence too low to rotate'
|
||||
else:
|
||||
action = 'no change'
|
||||
|
||||
facing = ''
|
||||
|
||||
if existing_rotation != 0:
|
||||
facing = f"with existing rotation {direction.get(existing_rotation, '?')}, "
|
||||
facing += f"page is facing {direction.get(orient_conf.angle, '?')}"
|
||||
|
||||
return f"{facing}, confidence {orient_conf.confidence:.2f} - {action}"
|
||||
|
||||
|
||||
def get_orientation_correction(preview: Path, page_context: PageContext):
|
||||
"""Work out orientation correct for each page.
|
||||
|
||||
We ask Ghostscript to draw a preview page, which will rasterize with the
|
||||
current /Rotate applied, and then ask OCR which way the page is
|
||||
oriented. If the value of /Rotate is correct (e.g., a user already
|
||||
manually fixed rotation), then OCR will say the page is pointing
|
||||
up and the correction is zero. Otherwise, the orientation found by
|
||||
OCR represents the clockwise rotation, or the counterclockwise
|
||||
correction to rotation.
|
||||
|
||||
When we draw the real page for OCR, we rotate it by the CCW correction,
|
||||
which points it (hopefully) upright. _graft.py takes care of the orienting
|
||||
the image and text layers.
|
||||
"""
|
||||
|
||||
orient_conf = page_context.plugin_manager.hook.get_ocr_engine().get_orientation(
|
||||
preview, page_context.options
|
||||
)
|
||||
|
||||
correction = orient_conf.angle % 360
|
||||
log.info(describe_rotation(page_context, orient_conf, correction))
|
||||
if (
|
||||
orient_conf.confidence >= page_context.options.rotate_pages_threshold
|
||||
and correction != 0
|
||||
):
|
||||
return correction
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def rasterize(
|
||||
input_file: Path,
|
||||
page_context: PageContext,
|
||||
correction: int = 0,
|
||||
output_tag: str = '',
|
||||
remove_vectors=None,
|
||||
):
|
||||
colorspaces = ['pngmono', 'pnggray', 'png256', 'png16m']
|
||||
device_idx = 0
|
||||
|
||||
if remove_vectors is None:
|
||||
remove_vectors = page_context.options.remove_vectors
|
||||
|
||||
output_file = page_context.get_path(f'rasterize{output_tag}.png')
|
||||
pageinfo = page_context.pageinfo
|
||||
|
||||
def at_least(cs):
|
||||
return max(device_idx, colorspaces.index(cs))
|
||||
|
||||
for image in pageinfo.images:
|
||||
if image.type_ != 'image':
|
||||
continue # ignore masks
|
||||
if image.bpc > 1:
|
||||
if image.color == Colorspace.index:
|
||||
device_idx = at_least('png256')
|
||||
elif image.color == Colorspace.gray:
|
||||
device_idx = at_least('pnggray')
|
||||
else:
|
||||
device_idx = at_least('png16m')
|
||||
|
||||
if pageinfo.has_vector:
|
||||
device_idx = at_least('png16m')
|
||||
|
||||
device = colorspaces[device_idx]
|
||||
|
||||
log.debug(f"Rasterize with {device}, rotation {correction}")
|
||||
|
||||
# Produce the page image with square resolution or else deskew and OCR
|
||||
# will not work properly.
|
||||
canvas_dpi = get_canvas_square_dpi(pageinfo, page_context.options)
|
||||
page_dpi = get_page_square_dpi(pageinfo, page_context.options)
|
||||
|
||||
page_context.plugin_manager.hook.rasterize_pdf_page(
|
||||
input_file=input_file,
|
||||
output_file=output_file,
|
||||
raster_device=device,
|
||||
raster_dpi=canvas_dpi,
|
||||
page_dpi=page_dpi,
|
||||
pageno=pageinfo.pageno + 1,
|
||||
rotation=correction,
|
||||
filter_vector=remove_vectors,
|
||||
)
|
||||
return output_file
|
||||
|
||||
|
||||
def preprocess_remove_background(input_file: Path, page_context: PageContext):
|
||||
if any(image.bpc > 1 for image in page_context.pageinfo.images):
|
||||
output_file = page_context.get_path('pp_rm_bg.png')
|
||||
leptonica.remove_background(input_file, output_file)
|
||||
return output_file
|
||||
else:
|
||||
log.info("background removal skipped on mono page")
|
||||
return input_file
|
||||
|
||||
|
||||
def preprocess_deskew(input_file: Path, page_context: PageContext):
|
||||
output_file = page_context.get_path('pp_deskew.png')
|
||||
dpi = get_page_square_dpi(page_context.pageinfo, page_context.options)
|
||||
leptonica.deskew(input_file, output_file, dpi.x)
|
||||
return output_file
|
||||
|
||||
|
||||
def preprocess_clean(input_file: Path, page_context: PageContext):
|
||||
output_file = page_context.get_path('pp_clean.png')
|
||||
dpi = get_page_square_dpi(page_context.pageinfo, page_context.options)
|
||||
unpaper.clean(
|
||||
input_file,
|
||||
output_file,
|
||||
dpi=dpi.x,
|
||||
unpaper_args=page_context.options.unpaper_args,
|
||||
)
|
||||
return output_file
|
||||
|
||||
|
||||
def create_ocr_image(image: Path, page_context: PageContext):
|
||||
"""Create the image we send for OCR. May not be the same as the display
|
||||
image depending on preprocessing. This image will never be shown to the
|
||||
user."""
|
||||
|
||||
output_file = page_context.get_path('ocr.png')
|
||||
options = page_context.options
|
||||
with Image.open(image) as im:
|
||||
white = ImageColor.getcolor('#ffffff', im.mode)
|
||||
# pink = ImageColor.getcolor('#ff0080', im.mode)
|
||||
draw = ImageDraw.ImageDraw(im)
|
||||
|
||||
log.debug('resolution %r', im.info['dpi'])
|
||||
|
||||
if not options.force_ocr:
|
||||
# Do not mask text areas when forcing OCR, because we need to OCR
|
||||
# all text areas
|
||||
mask = None # Exclude both visible and invisible text from OCR
|
||||
if options.redo_ocr:
|
||||
mask = True # Mask visible text, but not invisible text
|
||||
|
||||
for textarea in page_context.pageinfo.get_textareas(
|
||||
visible=mask, corrupt=None
|
||||
):
|
||||
# Calculate resolution based on the image size and page dimensions
|
||||
# without regard whatever resolution is in pageinfo (may differ or
|
||||
# be None)
|
||||
bbox = [float(v) for v in textarea]
|
||||
xyscale = tuple(float(coord) / 72.0 for coord in im.info['dpi'])
|
||||
pixcoords = (
|
||||
bbox[0] * xyscale[0],
|
||||
im.height - bbox[3] * xyscale[1],
|
||||
bbox[2] * xyscale[0],
|
||||
im.height - bbox[1] * xyscale[1],
|
||||
)
|
||||
log.debug('blanking %r', pixcoords)
|
||||
draw.rectangle(pixcoords, fill=white)
|
||||
# draw.rectangle(pixcoords, outline=pink)
|
||||
|
||||
if options.threshold:
|
||||
pix = leptonica.Pix.frompil(im)
|
||||
pix = pix.masked_threshold_on_background_norm()
|
||||
im_pix = pix.topil()
|
||||
im_pix.info['dpi'] = im.info['dpi']
|
||||
im = im_pix
|
||||
|
||||
del draw
|
||||
|
||||
filter_im = page_context.plugin_manager.hook.filter_ocr_image(
|
||||
page=page_context, image=im
|
||||
)
|
||||
if filter_im is not None:
|
||||
im = filter_im
|
||||
|
||||
# Pillow requires integer DPI
|
||||
dpi = tuple(round(coord) for coord in im.info['dpi'])
|
||||
im.save(output_file, dpi=dpi)
|
||||
return output_file
|
||||
|
||||
|
||||
def ocr_engine_hocr(input_file: Path, page_context: PageContext):
|
||||
hocr_out = page_context.get_path('ocr_hocr.hocr')
|
||||
hocr_text_out = page_context.get_path('ocr_hocr.txt')
|
||||
options = page_context.options
|
||||
|
||||
ocr_engine = page_context.plugin_manager.hook.get_ocr_engine()
|
||||
ocr_engine.generate_hocr(
|
||||
input_file=input_file,
|
||||
output_hocr=hocr_out,
|
||||
output_text=hocr_text_out,
|
||||
options=options,
|
||||
)
|
||||
return (hocr_out, hocr_text_out)
|
||||
|
||||
|
||||
def should_visible_page_image_use_jpg(pageinfo):
|
||||
# If all images were JPEGs originally, produce a JPEG as output
|
||||
return pageinfo.images and all(im.enc == Encoding.jpeg for im in pageinfo.images)
|
||||
|
||||
|
||||
def create_visible_page_jpg(image: Path, page_context: PageContext) -> Path:
|
||||
output_file = page_context.get_path('visible.jpg')
|
||||
with Image.open(image) as im:
|
||||
# At this point the image should be a .png, but deskew, unpaper
|
||||
# might have removed the DPI information. In this case, fall back to
|
||||
# square DPI used to rasterize. When the preview image was
|
||||
# rasterized, it was also converted to square resolution, which is
|
||||
# what we want to give to the OCR engine, so keep it square.
|
||||
if 'dpi' in im.info:
|
||||
dpi = Resolution(*im.info['dpi'])
|
||||
else:
|
||||
# Fallback to page-implied DPI
|
||||
dpi = get_page_square_dpi(page_context.pageinfo, page_context.options)
|
||||
|
||||
# Pillow requires integer DPI
|
||||
im.save(output_file, format='JPEG', dpi=dpi.to_int())
|
||||
return output_file
|
||||
|
||||
|
||||
def create_pdf_page_from_image(
|
||||
image: Path, page_context: PageContext, orientation_correction
|
||||
):
|
||||
# We rasterize a square DPI version of each page because most image
|
||||
# processing tools don't support rectangular DPI. Use the square DPI as it
|
||||
# accurately describes the image. It would be possible to resample the image
|
||||
# at this stage back to non-square DPI to more closely resemble the input,
|
||||
# except that the hocr renderer does not understand non-square DPI. The
|
||||
# sandwich renderer would be fine.
|
||||
output_file = page_context.get_path('visible.pdf')
|
||||
|
||||
pageinfo = page_context.pageinfo
|
||||
pagesize = 72.0 * float(pageinfo.width_inches), 72.0 * float(pageinfo.height_inches)
|
||||
effective_rotation = (pageinfo.rotation - orientation_correction) % 360
|
||||
if effective_rotation % 180 == 90:
|
||||
pagesize = pagesize[1], pagesize[0]
|
||||
|
||||
# This create a single page PDF
|
||||
with open(image, 'rb') as imfile, open(output_file, 'wb') as pdf:
|
||||
log.debug('convert')
|
||||
|
||||
layout_fun = img2pdf.get_layout_fun(pagesize)
|
||||
img2pdf.convert(
|
||||
imfile, with_pdfrw=False, layout_fun=layout_fun, outputstream=pdf
|
||||
)
|
||||
log.debug('convert done')
|
||||
|
||||
output_file = page_context.plugin_manager.hook.filter_pdf_page(
|
||||
page=page_context, image_filename=image, output_pdf=output_file
|
||||
)
|
||||
|
||||
return output_file
|
||||
|
||||
|
||||
def render_hocr_page(hocr: Path, page_context: PageContext):
|
||||
options = page_context.options
|
||||
output_file = page_context.get_path('ocr_hocr.pdf')
|
||||
dpi = get_page_square_dpi(page_context.pageinfo, options)
|
||||
debug_mode = options.pdf_renderer == 'hocrdebug'
|
||||
|
||||
hocrtransform = HocrTransform(hocr_filename=hocr, dpi=dpi.x) # square
|
||||
hocrtransform.to_pdf(
|
||||
out_filename=output_file,
|
||||
image_filename=None,
|
||||
show_bounding_boxes=False if not debug_mode else True,
|
||||
invisible_text=True if not debug_mode else False,
|
||||
interword_spaces=True,
|
||||
)
|
||||
return output_file
|
||||
|
||||
|
||||
def ocr_engine_textonly_pdf(input_image: Path, page_context: PageContext):
|
||||
output_pdf = page_context.get_path('ocr_tess.pdf')
|
||||
output_text = page_context.get_path('ocr_tess.txt')
|
||||
options = page_context.options
|
||||
|
||||
ocr_engine = page_context.plugin_manager.hook.get_ocr_engine()
|
||||
ocr_engine.generate_pdf(
|
||||
input_file=input_image,
|
||||
output_pdf=output_pdf,
|
||||
output_text=output_text,
|
||||
options=options,
|
||||
)
|
||||
return (output_pdf, output_text)
|
||||
|
||||
|
||||
def get_docinfo(base_pdf: pikepdf.Pdf, context: PdfContext) -> Dict[str, str]:
|
||||
options = context.options
|
||||
|
||||
def from_document_info(key):
|
||||
try:
|
||||
s = base_pdf.docinfo[key]
|
||||
return str(s)
|
||||
except (KeyError, TypeError):
|
||||
return ''
|
||||
|
||||
pdfmark = {
|
||||
k: from_document_info(k)
|
||||
for k in ('/Title', '/Author', '/Keywords', '/Subject', '/CreationDate')
|
||||
}
|
||||
if options is not None:
|
||||
if options.title:
|
||||
pdfmark['/Title'] = options.title
|
||||
if options.author:
|
||||
pdfmark['/Author'] = options.author
|
||||
if options.keywords:
|
||||
pdfmark['/Keywords'] = options.keywords
|
||||
if options.subject:
|
||||
pdfmark['/Subject'] = options.subject
|
||||
|
||||
creator_tag = context.plugin_manager.hook.get_ocr_engine().creator_tag(options)
|
||||
|
||||
pdfmark['/Creator'] = f'{PROGRAM_NAME} {VERSION} / {creator_tag}'
|
||||
pdfmark['/Producer'] = f'pikepdf {pikepdf.__version__}'
|
||||
pdfmark['/ModDate'] = encode_pdf_date(datetime.now(timezone.utc))
|
||||
return pdfmark
|
||||
|
||||
|
||||
def generate_postscript_stub(context: PdfContext):
|
||||
output_file = context.get_path('pdfa.ps')
|
||||
generate_pdfa_ps(output_file)
|
||||
return output_file
|
||||
|
||||
|
||||
def convert_to_pdfa(input_pdf: Path, input_ps_stub: Path, context: PdfContext):
|
||||
options = context.options
|
||||
input_pdfinfo = context.pdfinfo
|
||||
fix_docinfo_file = context.get_path('fix_docinfo.pdf')
|
||||
output_file = context.get_path('pdfa.pdf')
|
||||
|
||||
# If the DocumentInfo record contains NUL characters, Ghostscript will
|
||||
# produce XMP metadata which contains invalid XML entities (�).
|
||||
# NULs in DocumentInfo seem to be common since older Acrobats included them.
|
||||
# pikepdf can deal with this, but we make the world a better place by
|
||||
# stamping them out as soon as possible.
|
||||
modified = False
|
||||
with pikepdf.open(input_pdf) as pdf_file:
|
||||
try:
|
||||
len(pdf_file.docinfo)
|
||||
except TypeError:
|
||||
log.error(
|
||||
"File contains a malformed DocumentInfo block - continuing anyway"
|
||||
)
|
||||
else:
|
||||
if pdf_file.docinfo:
|
||||
for k, v in pdf_file.docinfo.items():
|
||||
if b'\x00' in bytes(v):
|
||||
pdf_file.docinfo[k] = bytes(v).replace(b'\x00', b'')
|
||||
modified = True
|
||||
if modified:
|
||||
pdf_file.save(fix_docinfo_file)
|
||||
else:
|
||||
safe_symlink(input_pdf, fix_docinfo_file)
|
||||
|
||||
context.plugin_manager.hook.generate_pdfa(
|
||||
pdf_version=input_pdfinfo.min_version,
|
||||
pdf_pages=[fix_docinfo_file],
|
||||
pdfmark=input_ps_stub,
|
||||
output_file=output_file,
|
||||
compression=options.pdfa_image_compression,
|
||||
pdfa_part=options.output_type[-1], # is pdfa-1, pdfa-2, or pdfa-3
|
||||
progressbar_class=(
|
||||
context.plugin_manager.hook.get_progressbar_class()
|
||||
if options.progress_bar
|
||||
else None
|
||||
),
|
||||
)
|
||||
|
||||
return output_file
|
||||
|
||||
|
||||
def should_linearize(working_file: Path, context: PdfContext):
|
||||
filesize = os.stat(working_file).st_size
|
||||
if filesize > (context.options.fast_web_view * 1_000_000):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def get_pdf_save_settings(output_type: str):
|
||||
if output_type == 'pdfa-1':
|
||||
# Trigger recompression to ensure object streams are removed, because
|
||||
# Acrobat complains about them in PDF/A-1b validation.
|
||||
return dict(
|
||||
preserve_pdfa=True,
|
||||
compress_streams=True,
|
||||
stream_decode_level=pikepdf.StreamDecodeLevel.generalized,
|
||||
object_stream_mode=pikepdf.ObjectStreamMode.disable,
|
||||
)
|
||||
else:
|
||||
return dict(
|
||||
preserve_pdfa=True,
|
||||
compress_streams=True,
|
||||
object_stream_mode=(pikepdf.ObjectStreamMode.generate),
|
||||
)
|
||||
|
||||
|
||||
def metadata_fixup(working_file: Path, context: PdfContext):
|
||||
output_file = context.get_path('metafix.pdf')
|
||||
options = context.options
|
||||
|
||||
def report_on_metadata(missing):
|
||||
if not missing:
|
||||
return
|
||||
if options.output_type.startswith('pdfa'):
|
||||
log.warning(
|
||||
"Some input metadata could not be copied because it is not "
|
||||
"permitted in PDF/A. You may wish to examine the output "
|
||||
"PDF's XMP metadata."
|
||||
)
|
||||
log.debug("The following metadata fields were not copied: %r", missing)
|
||||
else:
|
||||
log.error(
|
||||
"Some input metadata could not be copied."
|
||||
"You may wish to examine the output PDF's XMP metadata."
|
||||
)
|
||||
log.info("The following metadata fields were not copied: %r", missing)
|
||||
|
||||
with pikepdf.open(context.origin) as original, pikepdf.open(working_file) as pdf:
|
||||
docinfo = get_docinfo(original, context)
|
||||
with pdf.open_metadata() as meta:
|
||||
meta.load_from_docinfo(docinfo, delete_missing=False, raise_failure=False)
|
||||
# If xmp:CreateDate is missing, set it to the modify date to
|
||||
# match Ghostscript, for consistency
|
||||
if 'xmp:CreateDate' not in meta:
|
||||
meta['xmp:CreateDate'] = meta.get('xmp:ModifyDate', '')
|
||||
|
||||
with original.open_metadata(
|
||||
set_pikepdf_as_editor=False, update_docinfo=False, strict=False
|
||||
) as meta_original:
|
||||
if meta.get('dc:title') == 'Untitled':
|
||||
# Ghostscript likes to set title to Untitled if omitted from input.
|
||||
# Reverse this, because PDF/A TechNote 0003:Metadata in PDF/A-1
|
||||
# and the XMP Spec do not make this recommendation.
|
||||
if 'dc:title' not in meta_original:
|
||||
del meta['dc:title']
|
||||
missing = set(meta_original.keys()) - set(meta.keys())
|
||||
report_on_metadata(missing)
|
||||
|
||||
pdf.save(
|
||||
output_file,
|
||||
**get_pdf_save_settings(options.output_type),
|
||||
linearize=( # Don't linearize if optimize() will be linearizing too
|
||||
should_linearize(working_file, context)
|
||||
if options.optimize == 0
|
||||
else False
|
||||
),
|
||||
)
|
||||
|
||||
return output_file
|
||||
|
||||
|
||||
def optimize_pdf(input_file: Path, context: PdfContext, executor: Executor):
|
||||
output_file = context.get_path('optimize.pdf')
|
||||
save_settings = dict(
|
||||
linearize=should_linearize(input_file, context),
|
||||
**get_pdf_save_settings(context.options.output_type),
|
||||
)
|
||||
optimize(input_file, output_file, context, save_settings, executor)
|
||||
return output_file
|
||||
|
||||
|
||||
def enumerate_compress_ranges(iterable):
|
||||
skipped_from, index = None, None
|
||||
for index, txt_file in enumerate(iterable):
|
||||
index += 1
|
||||
if txt_file:
|
||||
if skipped_from is not None:
|
||||
yield (skipped_from, index - 1), None
|
||||
skipped_from = None
|
||||
yield (index, index), txt_file
|
||||
else:
|
||||
if skipped_from is None:
|
||||
skipped_from = index
|
||||
if skipped_from is not None:
|
||||
yield (skipped_from, index), None
|
||||
|
||||
|
||||
def merge_sidecars(txt_files: Iterable[Optional[Path]], context: PdfContext):
|
||||
output_file = context.get_path('sidecar.txt')
|
||||
with open(output_file, 'w', encoding="utf-8") as stream:
|
||||
for (frm, to), txt_file in enumerate_compress_ranges(txt_files):
|
||||
if frm != 1:
|
||||
stream.write('\f') # Form feed between pages
|
||||
if txt_file:
|
||||
with open(txt_file, encoding="utf-8") as in_:
|
||||
txt = in_.read()
|
||||
# Some OCR engines (e.g. Tesseract v4 alpha) add form feeds
|
||||
# between pages, and some do not. For consistency, we ignore
|
||||
# any added by the OCR engine and them on our own.
|
||||
if txt.endswith('\f'):
|
||||
stream.write(txt[:-1])
|
||||
else:
|
||||
stream.write(txt)
|
||||
else:
|
||||
if frm != to:
|
||||
pages = f'{frm}-{to}'
|
||||
else:
|
||||
pages = f'{frm}'
|
||||
stream.write(f'[OCR skipped on page(s) {pages}]')
|
||||
return output_file
|
||||
|
||||
|
||||
def copy_final(input_file, output_file, _context: PdfContext):
|
||||
log.debug('%s -> %s', input_file, output_file)
|
||||
with open(input_file, 'rb') as input_stream:
|
||||
if output_file == '-':
|
||||
copyfileobj(input_stream, sys.stdout.buffer)
|
||||
sys.stdout.flush()
|
||||
elif hasattr(output_file, 'writable'):
|
||||
output_stream = output_file
|
||||
copyfileobj(input_stream, output_stream)
|
||||
with suppress(AttributeError):
|
||||
output_stream.flush()
|
||||
else:
|
||||
# At this point we overwrite the output_file specified by the user
|
||||
# use copyfileobj because then we use open() to create the file and
|
||||
# get the appropriate umask, ownership, etc.
|
||||
with open(output_file, 'wb') as output_stream:
|
||||
copyfileobj(input_stream, output_stream)
|
||||
@@ -0,0 +1,122 @@
|
||||
# © 2020 James R. Barlow: github.com/jbarlow83
|
||||
#
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
import argparse
|
||||
import importlib
|
||||
import importlib.util
|
||||
import pkgutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import List, Tuple, Union
|
||||
|
||||
import pluggy
|
||||
|
||||
import ocrmypdf.builtin_plugins
|
||||
from ocrmypdf import pluginspec
|
||||
from ocrmypdf.cli import get_parser, plugins_only_parser
|
||||
|
||||
|
||||
class OcrmypdfPluginManager(pluggy.PluginManager):
|
||||
"""pluggy.PluginManager that can fork.
|
||||
|
||||
Capable of reconstructing itself in child workers.
|
||||
|
||||
Arguments:
|
||||
setup_func: callback that initializes the plugin manager with all
|
||||
standard plugins
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*args,
|
||||
plugins: List[Union[str, Path]],
|
||||
builtins: bool = True,
|
||||
**kwargs,
|
||||
):
|
||||
self.__init_args = args
|
||||
self.__init_kwargs = kwargs
|
||||
self.__plugins = plugins
|
||||
self.__builtins = builtins
|
||||
super().__init__(*args, **kwargs)
|
||||
self.setup_plugins()
|
||||
|
||||
def __getstate__(self):
|
||||
state = dict(
|
||||
init_args=self.__init_args,
|
||||
plugins=self.__plugins,
|
||||
builtins=self.__builtins,
|
||||
init_kwargs=self.__init_kwargs,
|
||||
)
|
||||
return state
|
||||
|
||||
def __setstate__(self, state):
|
||||
self.__init__(
|
||||
*state['init_args'],
|
||||
plugins=state['plugins'],
|
||||
builtins=state['builtins'],
|
||||
**state['init_kwargs'],
|
||||
)
|
||||
|
||||
def setup_plugins(self):
|
||||
self.add_hookspecs(pluginspec)
|
||||
|
||||
# 1. Register builtins
|
||||
if self.__builtins:
|
||||
for module in sorted(
|
||||
pkgutil.iter_modules(ocrmypdf.builtin_plugins.__path__)
|
||||
):
|
||||
name = f'ocrmypdf.builtin_plugins.{module.name}'
|
||||
module = importlib.import_module(name)
|
||||
self.register(module)
|
||||
|
||||
# 2. Install semfree if needed
|
||||
try:
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from multiprocessing.synchronize import SemLock
|
||||
|
||||
del SemLock
|
||||
except ImportError:
|
||||
self.register(importlib.import_module('ocrmypdf.extra_plugins.semfree'))
|
||||
|
||||
# 3. Register setuptools plugins
|
||||
self.load_setuptools_entrypoints('ocrmypdf')
|
||||
|
||||
# 4. Register plugins specified on command line
|
||||
for name in self.__plugins:
|
||||
if isinstance(name, Path) or name.endswith('.py'):
|
||||
# Import by filename
|
||||
module_name = Path(name).stem
|
||||
spec = importlib.util.spec_from_file_location(module_name, name)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[module_name] = module
|
||||
spec.loader.exec_module(module)
|
||||
else:
|
||||
# Import by dotted module name
|
||||
module = importlib.import_module(name)
|
||||
self.register(module)
|
||||
|
||||
|
||||
def get_plugin_manager(plugins: List[Union[str, Path]], builtins=True):
|
||||
pm = OcrmypdfPluginManager(
|
||||
project_name='ocrmypdf',
|
||||
plugins=plugins,
|
||||
builtins=builtins,
|
||||
)
|
||||
return pm
|
||||
|
||||
|
||||
def get_parser_options_plugins(
|
||||
args,
|
||||
) -> Tuple[argparse.ArgumentParser, argparse.Namespace, pluggy.PluginManager]:
|
||||
pre_options, _unused = plugins_only_parser.parse_known_args(args=args)
|
||||
plugin_manager = get_plugin_manager(pre_options.plugins)
|
||||
|
||||
parser = get_parser()
|
||||
plugin_manager.hook.add_options(parser=parser) # pylint: disable=no-member
|
||||
|
||||
options = parser.parse_args(args=args)
|
||||
return parser, options, plugin_manager
|
||||
@@ -0,0 +1,436 @@
|
||||
# © 2016 James R. Barlow: github.com/jbarlow83
|
||||
#
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
import logging
|
||||
import logging.handlers
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
from functools import partial
|
||||
from pathlib import Path
|
||||
from tempfile import mkdtemp
|
||||
from typing import List, NamedTuple, Optional, Tuple
|
||||
|
||||
import PIL
|
||||
|
||||
from ocrmypdf._concurrent import Executor, setup_executor
|
||||
from ocrmypdf._graft import OcrGrafter
|
||||
from ocrmypdf._jobcontext import PageContext, PdfContext, cleanup_working_files
|
||||
from ocrmypdf._logging import PageNumberFilter
|
||||
from ocrmypdf._pipeline import (
|
||||
convert_to_pdfa,
|
||||
copy_final,
|
||||
create_ocr_image,
|
||||
create_pdf_page_from_image,
|
||||
create_visible_page_jpg,
|
||||
generate_postscript_stub,
|
||||
get_orientation_correction,
|
||||
get_pdfinfo,
|
||||
is_ocr_required,
|
||||
merge_sidecars,
|
||||
metadata_fixup,
|
||||
ocr_engine_hocr,
|
||||
ocr_engine_textonly_pdf,
|
||||
optimize_pdf,
|
||||
preprocess_clean,
|
||||
preprocess_deskew,
|
||||
preprocess_remove_background,
|
||||
rasterize,
|
||||
rasterize_preview,
|
||||
render_hocr_page,
|
||||
should_visible_page_image_use_jpg,
|
||||
triage,
|
||||
validate_pdfinfo_options,
|
||||
)
|
||||
from ocrmypdf._plugin_manager import get_plugin_manager
|
||||
from ocrmypdf._validation import (
|
||||
check_requested_output_file,
|
||||
create_input_file,
|
||||
report_output_file_size,
|
||||
)
|
||||
from ocrmypdf.exceptions import ExitCode, ExitCodeException
|
||||
from ocrmypdf.helpers import (
|
||||
NeverRaise,
|
||||
available_cpu_count,
|
||||
check_pdf,
|
||||
pikepdf_enable_mmap,
|
||||
samefile,
|
||||
)
|
||||
from ocrmypdf.pdfa import file_claims_pdfa
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PageResult(NamedTuple): # pylint: disable=inherit-non-class
|
||||
pageno: int
|
||||
pdf_page_from_image: Optional[Path]
|
||||
ocr: Optional[Path]
|
||||
text: Optional[Path]
|
||||
orientation_correction: int
|
||||
|
||||
|
||||
tls = threading.local()
|
||||
tls.pageno = None
|
||||
|
||||
|
||||
old_factory = logging.getLogRecordFactory()
|
||||
|
||||
|
||||
def record_factory(*args, **kwargs):
|
||||
record = old_factory(*args, **kwargs)
|
||||
if hasattr(tls, 'pageno'):
|
||||
record.pageno = tls.pageno
|
||||
return record
|
||||
|
||||
|
||||
logging.setLogRecordFactory(record_factory)
|
||||
|
||||
|
||||
def preprocess(
|
||||
page_context: PageContext,
|
||||
image: Path,
|
||||
remove_background: bool,
|
||||
deskew: bool,
|
||||
clean: bool,
|
||||
) -> Path:
|
||||
if remove_background:
|
||||
image = preprocess_remove_background(image, page_context)
|
||||
if deskew:
|
||||
image = preprocess_deskew(image, page_context)
|
||||
if clean:
|
||||
image = preprocess_clean(image, page_context)
|
||||
return image
|
||||
|
||||
|
||||
def make_intermediate_images(
|
||||
page_context: PageContext, orientation_correction: int
|
||||
) -> Tuple[Path, Optional[Path]]:
|
||||
options = page_context.options
|
||||
|
||||
ocr_image = preprocess_out = None
|
||||
rasterize_out = rasterize(
|
||||
page_context.origin,
|
||||
page_context,
|
||||
correction=orientation_correction,
|
||||
remove_vectors=False,
|
||||
)
|
||||
|
||||
if not any([options.clean, options.clean_final, options.remove_vectors]):
|
||||
ocr_image = preprocess_out = preprocess(
|
||||
page_context,
|
||||
rasterize_out,
|
||||
options.remove_background,
|
||||
options.deskew,
|
||||
clean=False,
|
||||
)
|
||||
else:
|
||||
if not options.lossless_reconstruction:
|
||||
preprocess_out = preprocess(
|
||||
page_context,
|
||||
rasterize_out,
|
||||
options.remove_background,
|
||||
options.deskew,
|
||||
clean=options.clean_final,
|
||||
)
|
||||
if options.remove_vectors:
|
||||
rasterize_ocr_out = rasterize(
|
||||
page_context.origin,
|
||||
page_context,
|
||||
correction=orientation_correction,
|
||||
remove_vectors=True,
|
||||
output_tag='_ocr',
|
||||
)
|
||||
else:
|
||||
rasterize_ocr_out = rasterize_out
|
||||
|
||||
if (
|
||||
preprocess_out
|
||||
and rasterize_ocr_out == rasterize_out
|
||||
and options.clean == options.clean_final
|
||||
):
|
||||
# Optimization: image for OCR is identical to presentation image
|
||||
ocr_image = preprocess_out
|
||||
else:
|
||||
ocr_image = preprocess(
|
||||
page_context,
|
||||
rasterize_ocr_out,
|
||||
options.remove_background,
|
||||
options.deskew,
|
||||
clean=options.clean,
|
||||
)
|
||||
return ocr_image, preprocess_out
|
||||
|
||||
|
||||
def exec_page_sync(page_context: PageContext):
|
||||
options = page_context.options
|
||||
tls.pageno = page_context.pageno + 1
|
||||
|
||||
if not is_ocr_required(page_context):
|
||||
return PageResult(
|
||||
pageno=page_context.pageno,
|
||||
pdf_page_from_image=None,
|
||||
ocr=None,
|
||||
text=None,
|
||||
orientation_correction=0,
|
||||
)
|
||||
|
||||
orientation_correction = 0
|
||||
if options.rotate_pages:
|
||||
# Rasterize
|
||||
rasterize_preview_out = rasterize_preview(page_context.origin, page_context)
|
||||
orientation_correction = get_orientation_correction(
|
||||
rasterize_preview_out, page_context
|
||||
)
|
||||
|
||||
ocr_image, preprocess_out = make_intermediate_images(
|
||||
page_context, orientation_correction
|
||||
)
|
||||
ocr_image_out = create_ocr_image(ocr_image, page_context)
|
||||
|
||||
pdf_page_from_image_out = None
|
||||
if not options.lossless_reconstruction:
|
||||
assert preprocess_out
|
||||
visible_image_out = preprocess_out
|
||||
if should_visible_page_image_use_jpg(page_context.pageinfo):
|
||||
visible_image_out = create_visible_page_jpg(visible_image_out, page_context)
|
||||
filtered_image = page_context.plugin_manager.hook.filter_page_image(
|
||||
page=page_context, image_filename=visible_image_out
|
||||
)
|
||||
if filtered_image:
|
||||
visible_image_out = filtered_image
|
||||
pdf_page_from_image_out = create_pdf_page_from_image(
|
||||
visible_image_out, page_context, orientation_correction
|
||||
)
|
||||
|
||||
if options.pdf_renderer.startswith('hocr'):
|
||||
(hocr_out, text_out) = ocr_engine_hocr(ocr_image_out, page_context)
|
||||
ocr_out = render_hocr_page(hocr_out, page_context)
|
||||
elif options.pdf_renderer == 'sandwich':
|
||||
(ocr_out, text_out) = ocr_engine_textonly_pdf(ocr_image_out, page_context)
|
||||
else:
|
||||
raise NotImplementedError(f"pdf_renderer {options.pdf_renderer}")
|
||||
|
||||
return PageResult(
|
||||
pageno=page_context.pageno,
|
||||
pdf_page_from_image=pdf_page_from_image_out,
|
||||
ocr=ocr_out,
|
||||
text=text_out,
|
||||
orientation_correction=orientation_correction,
|
||||
)
|
||||
|
||||
|
||||
def post_process(pdf_file, context: PdfContext, executor: Executor):
|
||||
pdf_out = pdf_file
|
||||
if context.options.output_type.startswith('pdfa'):
|
||||
ps_stub_out = generate_postscript_stub(context)
|
||||
pdf_out = convert_to_pdfa(pdf_out, ps_stub_out, context)
|
||||
|
||||
pdf_out = metadata_fixup(pdf_out, context)
|
||||
return optimize_pdf(pdf_out, context, executor)
|
||||
|
||||
|
||||
def worker_init(max_pixels: int):
|
||||
# In Windows, child process will not inherit our change to this value in
|
||||
# the parent process, so ensure workers get it set. Not needed when running
|
||||
# threaded, but harmless to set again.
|
||||
PIL.Image.MAX_IMAGE_PIXELS = max_pixels
|
||||
pikepdf_enable_mmap()
|
||||
|
||||
|
||||
def exec_concurrent(context: PdfContext, executor: Executor):
|
||||
"""Execute the pipeline concurrently"""
|
||||
|
||||
# Run exec_page_sync on every page context
|
||||
options = context.options
|
||||
max_workers = min(len(context.pdfinfo), options.jobs)
|
||||
if max_workers > 1:
|
||||
log.info("Start processing %d pages concurrently", max_workers)
|
||||
|
||||
sidecars: List[Optional[Path]] = [None] * len(context.pdfinfo)
|
||||
ocrgraft = OcrGrafter(context)
|
||||
|
||||
def update_page(result: PageResult, pbar):
|
||||
try:
|
||||
tls.pageno = result.pageno + 1
|
||||
sidecars[result.pageno] = result.text
|
||||
pbar.update()
|
||||
ocrgraft.graft_page(
|
||||
pageno=result.pageno,
|
||||
image=result.pdf_page_from_image,
|
||||
textpdf=result.ocr,
|
||||
autorotate_correction=result.orientation_correction,
|
||||
)
|
||||
pbar.update()
|
||||
finally:
|
||||
tls.pageno = None
|
||||
|
||||
executor(
|
||||
use_threads=options.use_threads,
|
||||
max_workers=max_workers,
|
||||
tqdm_kwargs=dict(
|
||||
total=(2 * len(context.pdfinfo)),
|
||||
desc='OCR' if options.tesseract_timeout > 0 else 'Image processing',
|
||||
unit='page',
|
||||
unit_scale=0.5,
|
||||
disable=not options.progress_bar,
|
||||
),
|
||||
worker_initializer=partial(worker_init, PIL.Image.MAX_IMAGE_PIXELS),
|
||||
task=exec_page_sync,
|
||||
task_arguments=context.get_page_contexts(),
|
||||
task_finished=update_page,
|
||||
)
|
||||
|
||||
# Output sidecar text
|
||||
if options.sidecar:
|
||||
text = merge_sidecars(sidecars, context)
|
||||
# Copy text file to destination
|
||||
copy_final(text, options.sidecar, context)
|
||||
|
||||
if options.output_type != 'none':
|
||||
# Merge layers to one single pdf
|
||||
pdf = ocrgraft.finalize()
|
||||
|
||||
# PDF/A and metadata
|
||||
log.info("Postprocessing...")
|
||||
pdf = post_process(pdf, context, executor)
|
||||
|
||||
# Copy PDF file to destination
|
||||
copy_final(pdf, options.output_file, context)
|
||||
|
||||
|
||||
def configure_debug_logging(log_filename: Path, prefix: str = ''):
|
||||
"""
|
||||
Create a debug log file at a specified location.
|
||||
|
||||
Arguments:
|
||||
log_filename: Where to the put the log file.
|
||||
prefix: The logging domain prefix that should be sent to the log.
|
||||
"""
|
||||
log_file_handler = logging.FileHandler(log_filename, delay=True)
|
||||
log_file_handler.setLevel(logging.DEBUG)
|
||||
formatter = logging.Formatter(
|
||||
'[%(asctime)s] - %(name)s - %(levelname)7s -%(pageno)s %(message)s'
|
||||
)
|
||||
log_file_handler.setFormatter(formatter)
|
||||
log_file_handler.addFilter(PageNumberFilter())
|
||||
logging.getLogger(prefix).addHandler(log_file_handler)
|
||||
return log_file_handler
|
||||
|
||||
|
||||
def run_pipeline(options, *, plugin_manager, api=False):
|
||||
# Any changes to options will not take effect for options that are already
|
||||
# bound to function parameters in the pipeline. (For example
|
||||
# options.input_file, options.pdf_renderer are already bound.)
|
||||
if not options.jobs:
|
||||
options.jobs = available_cpu_count()
|
||||
if not plugin_manager:
|
||||
plugin_manager = get_plugin_manager(options.plugins)
|
||||
|
||||
work_folder = Path(mkdtemp(prefix="ocrmypdf.io."))
|
||||
debug_log_handler = None
|
||||
if (
|
||||
(options.keep_temporary_files or options.verbose >= 1)
|
||||
and not os.environ.get('PYTEST_CURRENT_TEST', '')
|
||||
and not api
|
||||
):
|
||||
# Debug log for command line interface only with verbose output
|
||||
# See https://github.com/pytest-dev/pytest/issues/5502 for why we skip this
|
||||
# when pytest is running
|
||||
debug_log_handler = configure_debug_logging(
|
||||
Path(work_folder) / "debug.log"
|
||||
) # pragma: no cover
|
||||
|
||||
pikepdf_enable_mmap()
|
||||
|
||||
executor = setup_executor(plugin_manager)
|
||||
try:
|
||||
check_requested_output_file(options)
|
||||
start_input_file, original_filename = create_input_file(options, work_folder)
|
||||
|
||||
# Triage image or pdf
|
||||
origin_pdf = triage(
|
||||
original_filename, start_input_file, work_folder / 'origin.pdf', options
|
||||
)
|
||||
|
||||
# Gather pdfinfo and create context
|
||||
pdfinfo = get_pdfinfo(
|
||||
origin_pdf,
|
||||
executor=executor,
|
||||
detailed_analysis=options.redo_ocr,
|
||||
progbar=options.progress_bar,
|
||||
max_workers=options.jobs if not options.use_threads else 1, # To help debug
|
||||
check_pages=options.pages,
|
||||
)
|
||||
|
||||
context = PdfContext(options, work_folder, origin_pdf, pdfinfo, plugin_manager)
|
||||
|
||||
# Validate options are okay for this pdf
|
||||
validate_pdfinfo_options(context)
|
||||
|
||||
# Execute the pipeline
|
||||
exec_concurrent(context, executor)
|
||||
|
||||
if options.output_file == '-':
|
||||
log.info("Output sent to stdout")
|
||||
elif (
|
||||
hasattr(options.output_file, 'writable') and options.output_file.writable()
|
||||
):
|
||||
log.info("Output written to stream")
|
||||
elif samefile(options.output_file, os.devnull):
|
||||
pass # Say nothing when sending to dev null
|
||||
else:
|
||||
if options.output_type.startswith('pdfa'):
|
||||
pdfa_info = file_claims_pdfa(options.output_file)
|
||||
if pdfa_info['pass']:
|
||||
log.info(
|
||||
"Output file is a %s (as expected)", pdfa_info['conformance']
|
||||
)
|
||||
else:
|
||||
log.warning(
|
||||
"Output file is okay but is not PDF/A (seems to be %s)",
|
||||
pdfa_info['conformance'],
|
||||
)
|
||||
return ExitCode.pdfa_conversion_failed
|
||||
if not check_pdf(options.output_file):
|
||||
log.warning('Output file: The generated PDF is INVALID')
|
||||
return ExitCode.invalid_output_pdf
|
||||
report_output_file_size(options, start_input_file, options.output_file)
|
||||
|
||||
except (KeyboardInterrupt if not api else NeverRaise):
|
||||
if options.verbose >= 1:
|
||||
log.exception("KeyboardInterrupt")
|
||||
else:
|
||||
log.error("KeyboardInterrupt")
|
||||
return ExitCode.ctrl_c
|
||||
except (ExitCodeException if not api else NeverRaise) as e:
|
||||
if options.verbose >= 1:
|
||||
log.exception("ExitCodeException")
|
||||
elif str(e):
|
||||
log.error("%s: %s", type(e).__name__, str(e))
|
||||
else:
|
||||
log.error(type(e).__name__)
|
||||
return e.exit_code
|
||||
except (PIL.Image.DecompressionBombError if not api else NeverRaise) as e:
|
||||
log.exception(
|
||||
"A decompression bomb error was encountered while executing the "
|
||||
"pipeline. Use the argument --max-image-mpixels to raise the maximum "
|
||||
"image pixel limit."
|
||||
)
|
||||
return ExitCode.other_error
|
||||
except (Exception if not api else NeverRaise): # pylint: disable=broad-except
|
||||
log.exception("An exception occurred while executing the pipeline")
|
||||
return ExitCode.other_error
|
||||
finally:
|
||||
if debug_log_handler:
|
||||
try:
|
||||
debug_log_handler.close()
|
||||
log.removeHandler(debug_log_handler)
|
||||
except OSError as e:
|
||||
print(e, file=sys.stderr)
|
||||
cleanup_working_files(work_folder, options)
|
||||
|
||||
return ExitCode.ok
|
||||
@@ -0,0 +1,118 @@
|
||||
# Copyright (c) 2014, Armin Ronacher
|
||||
#
|
||||
# Copyright (c) 2017, James R Barlow
|
||||
#
|
||||
# Some rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are
|
||||
# met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
#
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following
|
||||
# disclaimer in the documentation and/or other materials provided
|
||||
# with the distribution.
|
||||
#
|
||||
# * The names of the contributors may not be used to endorse or
|
||||
# promote products derived from this software without specific
|
||||
# prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
import codecs
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def verify_python3_env(): # pragma: no cover
|
||||
"""Ensures that the environment is good for unicode on Python 3."""
|
||||
|
||||
# PEP 538 changes in Python 3.7 should make this wrangling unnecessary
|
||||
if sys.version_info[0:3] >= (3, 7, 0):
|
||||
return
|
||||
|
||||
try:
|
||||
import locale
|
||||
|
||||
fs_enc = codecs.lookup(locale.getpreferredencoding()).name
|
||||
except Exception:
|
||||
fs_enc = 'ascii'
|
||||
if fs_enc != 'ascii':
|
||||
return
|
||||
|
||||
extra = ''
|
||||
if os.name == 'posix':
|
||||
import subprocess
|
||||
|
||||
rv = subprocess.run(
|
||||
['locale', '-a'], stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||
).stdout
|
||||
good_locales = set()
|
||||
has_c_utf8 = False
|
||||
|
||||
# Make sure we're operating on text here.
|
||||
if isinstance(rv, bytes):
|
||||
rv = rv.decode('ascii', 'replace')
|
||||
|
||||
for line in rv.splitlines():
|
||||
locale = line.strip()
|
||||
if locale.lower().endswith(('.utf-8', '.utf8')):
|
||||
good_locales.add(locale)
|
||||
if locale.lower() in ('c.utf8', 'c.utf-8'):
|
||||
has_c_utf8 = True
|
||||
|
||||
extra += '\n\n'
|
||||
if not good_locales:
|
||||
extra += (
|
||||
'Additional information: on this system no suitable UTF-8\n'
|
||||
'locales were discovered. This most likely requires resolving\n'
|
||||
'by reconfiguring the locale system.'
|
||||
)
|
||||
elif has_c_utf8:
|
||||
extra += (
|
||||
'This system supports the C.UTF-8 locale which is recommended.\n'
|
||||
'You might be able to resolve your issue by exporting the\n'
|
||||
'following environment variables:\n\n'
|
||||
' export LC_ALL=C.UTF-8\n'
|
||||
' export LANG=C.UTF-8'
|
||||
)
|
||||
else:
|
||||
extra += (
|
||||
'This system lists a couple of UTF-8 supporting locales that\n'
|
||||
'you can pick from. The following suitable locales were\n'
|
||||
'discovered: %s'
|
||||
) % ', '.join(sorted(good_locales))
|
||||
|
||||
bad_locale = None
|
||||
for locale in os.environ.get('LC_ALL'), os.environ.get('LANG'):
|
||||
if locale and locale.lower().endswith(('.utf-8', '.utf8')):
|
||||
bad_locale = locale
|
||||
if locale is not None:
|
||||
break
|
||||
if bad_locale is not None:
|
||||
extra += (
|
||||
'\nocrmypdf discovered that you exported a UTF-8 locale\n'
|
||||
'but the locale system could not pick up from it because\n'
|
||||
'it does not exist. The exported locale is "%s" but it\n'
|
||||
'is not supported'
|
||||
) % bad_locale
|
||||
|
||||
raise RuntimeError(
|
||||
'ocrmypdf will abort further execution because Python 3 '
|
||||
'was configured to use ASCII as encoding for the '
|
||||
'environment.' + extra
|
||||
)
|
||||
@@ -0,0 +1,433 @@
|
||||
#!/usr/bin/env python3
|
||||
# © 2015-17 James R. Barlow: github.com/jbarlow83
|
||||
#
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
import locale
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import unicodedata
|
||||
from pathlib import Path
|
||||
from shutil import copyfileobj
|
||||
from typing import List, Set, Tuple
|
||||
|
||||
import pikepdf
|
||||
import PIL
|
||||
|
||||
from ocrmypdf._exec import jbig2enc, pngquant, unpaper
|
||||
from ocrmypdf._unicodefun import verify_python3_env
|
||||
from ocrmypdf.exceptions import (
|
||||
BadArgsError,
|
||||
InputFileError,
|
||||
MissingDependencyError,
|
||||
OutputFileAccessError,
|
||||
)
|
||||
from ocrmypdf.helpers import is_file_writable, monotonic, safe_symlink, samefile
|
||||
from ocrmypdf.hocrtransform import HOCR_OK_LANGS
|
||||
from ocrmypdf.subprocess import check_external_program
|
||||
|
||||
# -------------
|
||||
# External dependencies
|
||||
|
||||
DEFAULT_LANGUAGE = 'eng' # Enforce English hegemony
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# --------
|
||||
# Critical environment tests
|
||||
verify_python3_env()
|
||||
|
||||
|
||||
def check_platform():
|
||||
if os.name == 'nt' and sys.maxsize <= 2 ** 32: # pragma: no cover
|
||||
# 32-bit interpreter on Windows
|
||||
log.error(
|
||||
"You are running OCRmyPDF in a 32-bit (x86) Python interpreter."
|
||||
"Please use a 64-bit (x86-64) version of Python."
|
||||
)
|
||||
|
||||
|
||||
def check_options_languages(options, ocr_engine_languages):
|
||||
if not options.languages:
|
||||
options.languages = {DEFAULT_LANGUAGE}
|
||||
system_lang = locale.getlocale()[0]
|
||||
if system_lang and not system_lang.startswith('en'):
|
||||
log.debug("No language specified; assuming --language %s", DEFAULT_LANGUAGE)
|
||||
if not ocr_engine_languages:
|
||||
return
|
||||
missing_languages = options.languages - ocr_engine_languages
|
||||
if missing_languages:
|
||||
msg = (
|
||||
"OCR engine does not have language data for the following "
|
||||
"requested languages: \n"
|
||||
)
|
||||
msg += '\n'.join(lang for lang in missing_languages)
|
||||
msg += '\nNote: most languages are identified by a 3-digit ISO 639-2 Code'
|
||||
raise MissingDependencyError(msg)
|
||||
|
||||
|
||||
def check_options_output(options):
|
||||
is_latin = options.languages.issubset(HOCR_OK_LANGS)
|
||||
|
||||
if options.pdf_renderer.startswith('hocr') and not is_latin:
|
||||
log.warning(
|
||||
"The 'hocr' PDF renderer is known to cause problems with one "
|
||||
"or more of the languages in your document. Use "
|
||||
"`--pdf-renderer auto` (the default) to avoid this issue."
|
||||
)
|
||||
|
||||
if options.output_type == 'none' and options.output_file != os.devnull:
|
||||
raise BadArgsError(
|
||||
"Since you specified `--pdf-renderer none`, the output file "
|
||||
f"{options.output_file} cannot be produced. Set the output file to "
|
||||
f"{os.devnull} to suppress this message."
|
||||
)
|
||||
|
||||
lossless_reconstruction = False
|
||||
if not any(
|
||||
(
|
||||
options.deskew,
|
||||
options.clean_final,
|
||||
options.force_ocr,
|
||||
options.remove_background,
|
||||
)
|
||||
):
|
||||
lossless_reconstruction = True
|
||||
options.lossless_reconstruction = lossless_reconstruction
|
||||
|
||||
if not options.lossless_reconstruction and options.redo_ocr:
|
||||
raise BadArgsError(
|
||||
"--redo-ocr is not currently compatible with --deskew, "
|
||||
"--clean-final, and --remove-background"
|
||||
)
|
||||
|
||||
|
||||
def check_options_sidecar(options):
|
||||
if options.sidecar == '\0':
|
||||
if options.output_file == '-':
|
||||
raise BadArgsError(
|
||||
"--sidecar filename must be specified when output file is stdout."
|
||||
)
|
||||
elif options.output_file == os.devnull:
|
||||
raise BadArgsError(
|
||||
"--sidecar filename must be specified when output file is /dev/null or NUL."
|
||||
)
|
||||
options.sidecar = options.output_file + '.txt'
|
||||
if options.sidecar == options.input_file or options.sidecar == options.output_file:
|
||||
raise BadArgsError(
|
||||
"--sidecar file must be different from the input and output files"
|
||||
)
|
||||
|
||||
|
||||
def check_options_preprocessing(options):
|
||||
if options.clean_final:
|
||||
options.clean = True
|
||||
if options.unpaper_args and not options.clean:
|
||||
raise BadArgsError("--clean is required for --unpaper-args")
|
||||
if options.clean:
|
||||
check_external_program(
|
||||
program='unpaper',
|
||||
package='unpaper',
|
||||
version_checker=unpaper.version,
|
||||
need_version='6.1',
|
||||
required_for=['--clean, --clean-final'],
|
||||
)
|
||||
try:
|
||||
if options.unpaper_args:
|
||||
options.unpaper_args = unpaper.validate_custom_args(
|
||||
options.unpaper_args
|
||||
)
|
||||
except Exception as e:
|
||||
raise BadArgsError("--unpaper-args: " + str(e)) from e
|
||||
|
||||
|
||||
def _pages_from_ranges(ranges: str) -> Set[int]:
|
||||
pages: List[int] = []
|
||||
page_groups = ranges.replace(' ', '').split(',')
|
||||
for g in page_groups:
|
||||
if not g:
|
||||
continue
|
||||
try:
|
||||
start, end = g.split('-')
|
||||
except ValueError:
|
||||
pages.append(int(g) - 1)
|
||||
else:
|
||||
try:
|
||||
new_pages = list(range(int(start) - 1, int(end)))
|
||||
if not new_pages:
|
||||
raise BadArgsError(f"invalid page subrange '{start}-{end}'")
|
||||
pages.extend(new_pages)
|
||||
except ValueError:
|
||||
raise BadArgsError("invalid page range") from None
|
||||
|
||||
if not pages:
|
||||
raise BadArgsError(
|
||||
f"The string of page ranges '{ranges}' did not contain any recognizable "
|
||||
f"page ranges."
|
||||
)
|
||||
|
||||
if not monotonic(pages):
|
||||
log.warning(
|
||||
"List of pages to process contains duplicate pages, or pages that are "
|
||||
"out of order"
|
||||
)
|
||||
if any(page < 0 for page in pages):
|
||||
raise BadArgsError("pages refers to a page number less than 1")
|
||||
|
||||
log.debug("OCRing only these pages: %s", pages)
|
||||
return set(pages)
|
||||
|
||||
|
||||
def check_options_ocr_behavior(options):
|
||||
exclusive_options = sum(
|
||||
(1 if opt else 0)
|
||||
for opt in (options.force_ocr, options.skip_text, options.redo_ocr)
|
||||
)
|
||||
if exclusive_options >= 2:
|
||||
raise BadArgsError("Choose only one of --force-ocr, --skip-text, --redo-ocr.")
|
||||
if options.pages:
|
||||
options.pages = _pages_from_ranges(options.pages)
|
||||
|
||||
|
||||
def check_options_optimizing(options):
|
||||
if options.optimize >= 2:
|
||||
check_external_program(
|
||||
program='pngquant',
|
||||
package='pngquant',
|
||||
version_checker=pngquant.version,
|
||||
need_version='2.0.1',
|
||||
required_for='--optimize {2,3}',
|
||||
)
|
||||
|
||||
if options.optimize >= 2:
|
||||
# Although we use JBIG2 for optimize=1, don't nag about it unless the
|
||||
# user is asking for more optimization
|
||||
check_external_program(
|
||||
program='jbig2',
|
||||
package='jbig2enc',
|
||||
version_checker=jbig2enc.version,
|
||||
need_version='0.28',
|
||||
required_for='--optimize {2,3} | --jbig2-lossy',
|
||||
recommended=True if not options.jbig2_lossy else False,
|
||||
)
|
||||
|
||||
if options.optimize == 0 and any(
|
||||
[options.jbig2_lossy, options.png_quality, options.jpeg_quality]
|
||||
):
|
||||
log.warning(
|
||||
"The arguments --jbig2-lossy, --png-quality, and --jpeg-quality "
|
||||
"will be ignored because --optimize=0."
|
||||
)
|
||||
|
||||
|
||||
def check_options_advanced(options):
|
||||
if options.pdfa_image_compression != 'auto' and not options.output_type.startswith(
|
||||
'pdfa'
|
||||
):
|
||||
log.warning(
|
||||
"--pdfa-image-compression argument only applies when "
|
||||
"--output-type is one of 'pdfa', 'pdfa-1', or 'pdfa-2'"
|
||||
)
|
||||
|
||||
|
||||
def check_options_metadata(options):
|
||||
docinfo = [options.title, options.author, options.keywords, options.subject]
|
||||
for s in (m for m in docinfo if m):
|
||||
for c in s:
|
||||
if unicodedata.category(c) == 'Co' or ord(c) >= 0x10000:
|
||||
raise ValueError(
|
||||
"One of the metadata strings contains "
|
||||
"an unsupported Unicode character: '{}' (U+{})".format(
|
||||
c, hex(ord(c))[2:].upper()
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def check_options_pillow(options):
|
||||
PIL.Image.MAX_IMAGE_PIXELS = int(options.max_image_mpixels * 1_000_000)
|
||||
if PIL.Image.MAX_IMAGE_PIXELS == 0:
|
||||
PIL.Image.MAX_IMAGE_PIXELS = None
|
||||
|
||||
|
||||
def _check_options(options, plugin_manager, ocr_engine_languages):
|
||||
check_platform()
|
||||
check_options_languages(options, ocr_engine_languages)
|
||||
check_options_metadata(options)
|
||||
check_options_output(options)
|
||||
check_options_sidecar(options)
|
||||
check_options_preprocessing(options)
|
||||
check_options_ocr_behavior(options)
|
||||
check_options_optimizing(options)
|
||||
check_options_advanced(options)
|
||||
check_options_pillow(options)
|
||||
plugin_manager.hook.check_options(options=options)
|
||||
|
||||
|
||||
def check_options(options, plugin_manager):
|
||||
ocr_engine_languages = plugin_manager.hook.get_ocr_engine().languages(options)
|
||||
_check_options(options, plugin_manager, ocr_engine_languages)
|
||||
|
||||
|
||||
def check_closed_streams(options): # pragma: no cover
|
||||
"""Work around Python issue with multiprocessing forking on closed streams
|
||||
|
||||
https://bugs.python.org/issue28326
|
||||
|
||||
Attempting to a fork/exec a new Python process when any of std{in,out,err}
|
||||
are closed or not flushable for some reason may raise an exception.
|
||||
Fix this by opening devnull if the handle seems to be closed. Do this
|
||||
globally to avoid tracking all places that fork.
|
||||
|
||||
Seems to be specific to multiprocessing.Process not all Python process
|
||||
forkers.
|
||||
|
||||
The error actually occurs when the stream object is not flushable,
|
||||
but replacing an open stream object that is not flushable with
|
||||
/dev/null is a bad idea since it will create a silent failure. Replacing
|
||||
a closed handle with /dev/null seems safe.
|
||||
|
||||
"""
|
||||
|
||||
if sys.version_info[0:3] >= (3, 6, 4):
|
||||
return True # Issued fixed in Python 3.6.4+
|
||||
|
||||
if sys.stderr is None:
|
||||
sys.stderr = open(os.devnull, 'w')
|
||||
|
||||
if sys.stdin is None:
|
||||
if options.input_file == '-':
|
||||
log.error("Trying to read from stdin but stdin seems closed")
|
||||
return False
|
||||
sys.stdin = open(os.devnull)
|
||||
|
||||
if sys.stdout is None:
|
||||
if options.output_file == '-':
|
||||
# Can't replace stdout if the user is piping
|
||||
# If this case can even happen, it must be some kind of weird
|
||||
# stream.
|
||||
log.error(
|
||||
"Output was set to stdout '-' but the stream attached to "
|
||||
"stdout does not support the flush() system call. This "
|
||||
"will fail."
|
||||
)
|
||||
return False
|
||||
sys.stdout = open(os.devnull, 'w')
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def create_input_file(options, work_folder: Path) -> Tuple[Path, str]:
|
||||
if options.input_file == '-':
|
||||
# stdin
|
||||
log.info('reading file from standard input')
|
||||
target = work_folder / 'stdin'
|
||||
with open(target, 'wb') as stream_buffer:
|
||||
copyfileobj(sys.stdin.buffer, stream_buffer)
|
||||
return target, "stdin"
|
||||
elif hasattr(options.input_file, 'readable'):
|
||||
if not options.input_file.readable():
|
||||
raise InputFileError("Input file stream is not readable")
|
||||
log.info('reading file from input stream')
|
||||
target = work_folder / 'stream'
|
||||
with open(target, 'wb') as stream_buffer:
|
||||
copyfileobj(options.input_file, stream_buffer)
|
||||
return target, "stream"
|
||||
else:
|
||||
try:
|
||||
target = work_folder / 'origin'
|
||||
safe_symlink(options.input_file, target)
|
||||
return target, os.fspath(options.input_file)
|
||||
except FileNotFoundError:
|
||||
msg = f"File not found - {options.input_file}"
|
||||
if Path('/.dockerenv').exists(): # pragma: no cover
|
||||
msg += (
|
||||
"\nDocker cannot your working directory unless you "
|
||||
"explicitly share it with the Docker container and set up"
|
||||
"permissions correctly.\n"
|
||||
"You may find it easier to use stdin/stdout:"
|
||||
"\n"
|
||||
"\tdocker run -i --rm jbarlow83/ocrmypdf - - <input.pdf >output.pdf\n"
|
||||
)
|
||||
raise InputFileError(msg)
|
||||
|
||||
|
||||
def check_requested_output_file(options):
|
||||
if options.output_file == '-':
|
||||
if sys.stdout.isatty():
|
||||
raise BadArgsError(
|
||||
"Output was set to stdout '-' but it looks like stdout "
|
||||
"is connected to a terminal. Please redirect stdout to a "
|
||||
"file."
|
||||
)
|
||||
elif hasattr(options.output_file, 'writable'):
|
||||
if not options.output_file.writable():
|
||||
raise OutputFileAccessError("Output stream is not writable")
|
||||
elif not is_file_writable(options.output_file):
|
||||
raise OutputFileAccessError(
|
||||
f"Output file location ({options.output_file}) is not a writable file."
|
||||
)
|
||||
|
||||
|
||||
def report_output_file_size(options, input_file, output_file):
|
||||
try:
|
||||
output_size = Path(output_file).stat().st_size
|
||||
input_size = Path(input_file).stat().st_size
|
||||
except FileNotFoundError:
|
||||
return # Outputting to stream or something
|
||||
with pikepdf.open(output_file) as p:
|
||||
# Overhead constants obtained by estimating amount of data added by OCR
|
||||
# PDF/A conversion, and possible XMP metadata addition, with compression
|
||||
FILE_OVERHEAD = 4000
|
||||
OCR_PER_PAGE_OVERHEAD = 3000
|
||||
reasonable_overhead = FILE_OVERHEAD + OCR_PER_PAGE_OVERHEAD * len(p.pages)
|
||||
ratio = output_size / input_size
|
||||
reasonable_ratio = output_size / (input_size + reasonable_overhead)
|
||||
if reasonable_ratio < 1.35 or input_size < 25000:
|
||||
return # Seems fine
|
||||
|
||||
reasons = []
|
||||
image_preproc = {
|
||||
'deskew',
|
||||
'clean_final',
|
||||
'remove_background',
|
||||
'oversample',
|
||||
'force_ocr',
|
||||
}
|
||||
for arg in image_preproc:
|
||||
if getattr(options, arg, False):
|
||||
reasons.append(
|
||||
f"The argument --{arg.replace('_', '-')} was issued, causing transcoding."
|
||||
)
|
||||
|
||||
if options.optimize == 0:
|
||||
reasons.append("Optimization was disabled.")
|
||||
else:
|
||||
image_optimizers = {
|
||||
'jbig2': jbig2enc.available(),
|
||||
'pngquant': pngquant.available(),
|
||||
}
|
||||
for name, available in image_optimizers.items():
|
||||
if not available:
|
||||
reasons.append(
|
||||
f"The optional dependency '{name}' was not found, so some image "
|
||||
f"optimizations could not be attempted."
|
||||
)
|
||||
if options.output_type.startswith('pdfa'):
|
||||
reasons.append("PDF/A conversion was enabled. (Try `--output-type pdf`.)")
|
||||
if options.plugins:
|
||||
reasons.append("Plugins were used.")
|
||||
|
||||
if reasons:
|
||||
explanation = "Possible reasons for this include:\n" + '\n'.join(reasons) + "\n"
|
||||
else:
|
||||
explanation = "No reason for this increase is known. Please report this issue."
|
||||
|
||||
log.warning(
|
||||
f"The output file size is {ratio:.2f}× larger than the input file.\n"
|
||||
f"{explanation}"
|
||||
)
|
||||