commit 2f5ac4040c3f647bb9962e15e94f2bbba2c4ee39 Author: zhouganqing Date: Thu Nov 17 16:33:46 2022 +0800 Import Upstream version 2.7 diff --git a/KEYS b/KEYS new file mode 100644 index 0000000..9c38546 --- /dev/null +++ b/KEYS @@ -0,0 +1,478 @@ +This file contains the PGP keys of various developers. +Please don't use them for email unless you have to. Their main +purpose is code signing. + +Users: + pgp < KEYS + (gpg --import < KEYS) +Developers: + pgp -kxa and append it to this file. + (pgpk -ll && pgpk -xa ) >> this file. + (gpg --list-keys + && gpg --armor --export ) >> this file. + +pub 1024D/8408F755 2003-01-20 Christian Geisert +sub 1024g/41D98B12 2003-01-20 +pub 1024D/7C611584 2005-07-19 Jeremias M�rki +sub 2048g/C0F1AD34 2005-07-19 +pub 1024D/4358C584 2006-12-08 Vincent Hennebert +sub 2048g/0BD6AC9B 2006-12-08 +pub 1024D/CC31AE97 2008-03-27 [expires: 2011-01-01] +uid Maximilian Berger +pub 1024R/C93C5700 2008-07-12 +uid Maximilian Berger +pub 4096R/72FA275A 2014-07-22 +uid Vincent Hennebert +sub 4096R/EC50D2FD 2014-07-22 + + +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1.0.6 (GNU/Linux) +Comment: Weitere Infos: siehe http://www.gnupg.org + +mQGiBD4rT6kRBADPozVPJAOKLqcgtnGFHj0Qav5OBPqjTprJji0BgxPrcZLZQFUO +0AmeJpzwgE0vDISx+DgGOKPcqeCLnSqtoR3rYc5Tf57FPlZ/E2/fzMwC0soYzJ6e +E9uSsXBIPeeRHcrxyCYJx69g2D/zS2vGnAIr0LYLXhadvChK1zngU0pFswCg5z0a +TjCH+9VWzerOLaEQYqVyGgMD/i456FFD2lWes8amcN+wpBLSpxysyrbu1FOZ3sVi +MjYsODDeLKRB1Cxn6UMx7WE+Av+UhtoJkoTsvcGBtkj+rFfEav2WmCsYDvLwbqpE +P5Pun7qgXbznbzCqdNUavXM0goBQtyFhtGkJSoOVXM6H5raO3THEPXU971gkOC9+ +agZDA/0WdV3VumDwi9DvII58Fv8K5HKxkZrLeUWAYJzgzeroYgfvuNKr/SrAFuyR +W8XxDMPMAMvO1qKUU1Zjffx6RUjeiptmF4ozLvU3I/Pr6atWJL8SPH6rrk6xDpZ5 +Ev/we0MmRU5kk9Y8KGavAsOjbMbTv77DL2AZ8qapUQYqfuuiR7QlQ2hyaXN0aWFu +IEdlaXNlcnQgPGNocmlzZ0BhcGFjaGUub3JnPohXBBMRAgAXBQI+K0+pBQsHCgME +AxUDAgMWAgECF4AACgkQc84u+4QI91W/OwCffMPR+FpADYlte9AabXRuzomhdh4A +oNvl89kJdg43/nMw5CErpMvMFNjVuQENBD4rT6sQBACr7d7g+yOPuYaFeS9+wXlO +vz3gVWZNUOPalrJwWPQ2yXxCzEd4chpBDUsq7TusEe/RCSUuSQYe0FAET2der4Uj +6TJPB7SuJos3hrqsUQOluoEHS8bCPD7mpXIjd1+tQu57jxgcW4/VnEn3/c57blgY +V2/rpAUE8nDRPB1Z8oveJwAECwQAp0zOTs1ehN4MFzy/3T1xO8HVN7LiaZSc4mEs +oyIcEcD3iwxJqsVPmJLOfP8fhuqyv4QfG+7gdNBZqji5WTUK2o1vYgnEbvsnGDst +Lx9kwaWCalGMXdbuEzXoheX2JJCeP8YkPQw/r9rCeP0jLXExU3B6RJkDXBrZB7L3 +Jdj2mxKIRgQYEQIABgUCPitPqwAKCRBzzi77hAj3VeUxAKCJXCs0vrCcfMSO/2M/ +Eq/rMruVPwCg34SJtFP5wtlgHa7kdhBLKoenD7g= +=+cef +-----END PGP PUBLIC KEY BLOCK----- + +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1.4.2 (MingW32) + +mQGiBELdQqsRBACPvRuWTsO4Sv95wSFzaMJcSJs81b9lpAT4BKsi3K/1y7pcxTJW +IKP5Khc6cOom3nnRDofEh1KcLnvx+X+A+WF5YF7c1qd4EG3tWgjzqnkT9NUsw6t/ +3yAHL9iHr5nB7aZuiDdXgsLL5F2wNjuiSrTEDVM1wSm+QLuqqJ2lSJOcCwCg97BA +yyX59XauxdQNfF00pfYIW3UD/R2TzMD5YxHbCO2Ib3j5O2cJU+G56q1krLwFKSzq +XsiKOh2+8Bvwj2bBlQ6uYMwZc08WOgtiG3uRMDsdPE3JIoDogIguuADzO1LFFoi5 +mAEizqCN+8OSZ6luVeznUKCObF+yJvqJyG9RoDgv+v8KdqVz5NZ2BdtcY7LxHGKI +6k/LA/9WH1p8N10GETyagzNtU1tDMZGk8zB5C1qE9hgMDr5iRKrFuGnwBzMseKOj +nQ4tPW47hWYeXLaou6JPTRhGP0wITnOoYmcP5ymgOPZJZwARfC3qXznrdOAmL8D2 +x+W7DP7ZtKPCYDa0norSEq3VPSUWjAjAPvskgiN6u6Y5/0J487QlSmVyZW1pYXMg +TcOkcmtpIDxqZXJlbWlhc0BtYWVya2kub3JnPoheBBMRAgAeBQJC3VVHAhsjBgsJ +CAcDAgMVAgMDFgIBAh4BAheAAAoJEPcwlXB8YRWECjoAnA90rdo+Ob3k1BOMz5cK +PaQZtuuiAKCHOPtp1foBDjGPBA0FR9MtSCmMLYhGBBIRAgAGBQJC31YgAAoJEKIR +WuFfa4tyKSsAnj6YCONElnEDA5vaa1lJES4UcVAeAJ9JG4bXlEPUqyBPhOtvetVS +OWtNGIhGBBMRAgAGBQJC7u6uAAoJEME58VMjy3oqElwAnijYebpmsWIw86ilU2Zf +epTWKSt1AJ0cMQZmjm/WrGI68rCRcEL7X2wTOYhGBBMRAgAGBQJDK/iDAAoJEDf2 +j/UBWvyKp3YAniyZxx/ee9g7asY2AR87yAVWPRjQAJ0U4l3dC/8ugLmyE7RbkFqL +iALoRLQpSmVyZW1pYXMgTcOkcmtpIDxpbmZvQGplcmVtaWFzLW1hZXJraS5jaD6I +XgQTEQIAHgUCQt1VXQIbIwYLCQgHAwIDFQIDAxYCAQIeAQIXgAAKCRD3MJVwfGEV +hNNHAKDkYf0faxpqpmnwtFNeFETmTqd7HACeK0jaTfp7b/VhTlFmF+cSkrO4lK2I +RgQSEQIABgUCQt9WIAAKCRCiEVrhX2uLcpUWAKClQPvTzFXFlDk/nDBa0wxesi4n +qwCfWiXArQAoIm/hG9zpK+h2zHwlYg6IRgQTEQIABgUCQu7urgAKCRDBOfFTI8t6 +KpDiAKCCPK5ixmDxrKvvIKnx7hofZ0LiAQCfV6vfRmuVmfWo2LMMaa9xw5WBtUKI +RgQTEQIABgUCQyv4gwAKCRA39o/1AVr8iqV2AJ9sBMWQ6YjZHIfBp+aoo84EI/Kg +6wCfRDkrpQhjpeSM4DPaRiKgL1LQrVm0JUplcmVtaWFzIE3DpHJraSA8amVyZW1p +YXNAYXBhY2hlLm9yZz6IXgQTEQIAHgUCQt1VgwIbIwYLCQgHAwIDFQIDAxYCAQIe +AQIXgAAKCRD3MJVwfGEVhAjAAKDme5j7fzFk365pOZm1lJyEHMJuPACgspAO3iPV +7G659xb8l8IQQIZn18CIRgQSEQIABgUCQt9WGAAKCRCiEVrhX2uLcirOAKCwgb9x +2zr8Q3j7KnD9XSgqrwv33ACgzcUOgabzbXlJWEOiW4d2LIK4zjyIRgQTEQIABgUC +Qu7uqQAKCRDBOfFTI8t6KuNXAJ40TlT/Goa/tV/5zL+ZH6OdWjTImACghZxdWnD+ +CLZORMpBFYUlUKffSlWIRgQTEQIABgUCQyv4bAAKCRA39o/1AVr8iv1/AKCX8NY8 +nffFDHsck7SMPWdb8toSvwCfd0Yf8OOYq3/O8JidWNpnNTAZabqIYQQTEQIAIQIb +IwYLCQgHAwIDFQIDAxYCAQIeAQIXgAUCQt1VmwIZAQAKCRD3MJVwfGEVhOZyAJ9G +J6pU0t9yrsVkuHcAkhF1YG6RDgCdFOb6NohfGeBbOraUGdQvKwJ3J0W5Ag0EQt1C +vhAIAIhBOvTiliy95oxWLplZzCiXq6Dm3lYUOhDxmioKavI6KH2FzdnrHHvz91ES +f7tGUNizHQyv4+zSAv8B8JmKiQ7Tk6DL5yJjZ6otwyOuR0ZIviXZwYirT8lOO4wP +m3oV1V6Uf1oIwZQ2Y1fTFO4+8UPlTT0IUNMxuIIbvkRSMup6FIFGDoJQ/IYtBB4G +4mo6DMilyNzDiaqJ5k+iSY/GcCIzdWUd7ryr7344DffnJHg0CDynR/gpISWWj0sf +YCnjZJANrVGV7O5WnKsVKqzTIRfJKSGBoB1ekvQ8AtGq1X8DfGgKubjlzGjCNbeW +YiKNEa/M4DOAwhFzvftGueuzujMAAwUH/1+A8nfA3YgoG8wGPifhXBrB5aUx5I12 +1akJvK4KQaIFE3dDRRYtwQHp0EyKB85z3lTlmdOupE5oKHd3ZYeeaQSUnt5i+dtz +Xxz3RLBB6NYlcYJJyqSb31rTAMoq1iultLYbH+ugK/kUPTIOslvUJ/q4J22NJ/wS +aIEqBlz/RczCAniNooQ/2ie733MpJ6NfROGIiOrNsRlQw+7P9TOmB3FxKLtzIunD +F/Oo/4vznSoaVSUtVFMCKM2tLLbVV2LWBm9ztakfOXwmV0aoveAZ6a0J5+9RbC82 +HJW5Sez4fLZxK7tENCQoscjkjM/xfsDz5mz/8JF4T0tLehUjKrLBlsaIRgQYEQIA +BgUCQt1CvgAKCRD3MJVwfGEVhD0+AJwLiZ/2jUDCKVngO+lqNscX8TQ0LQCeM4DI +UKHCLjnj2UtX8OfockTGIic= +=1nMU +-----END PGP PUBLIC KEY BLOCK----- + +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1.4.2.2 (GNU/Linux) + +mQGiBEV5mbgRBACBd9xGOGzs8ah/N11zlDi8woJUh02EgztXdMfij4F3u12DkneC +OIwGH14fWHdkhKjwYMi5LQfvBsnd3P5v5PTBxYati7ZQDDjvYsAJiMXbyUdszdyQ +ig/UuNMwdB5YBrdtklzZcOuiNt/yeoocURQRwkwklsVBIYWwovcdXLTRWwCglPVQ +0NrC7VPRPTgK0Y1wxOgwzj0D/RpwFEAcl1SJHoOwhwKykNzA05YABaxXhksKi2qQ +C8M4mUofqweU3ocU0tBqQAR351n7hQWAaIs6ScOQtcKPJj9SV8SQgqxwj7WfHscj +9X3lkP6cxstW+W8SblTgrKwl7JLLkja1u1cNUeD0QzWImuRBpOjh4s97ZFwSHRoH +hCLgA/4/GDXbOhC4Wi9i8HHQNhEA6l86ZBteGq5u4SW57cK69mnOGj8iBxenIWeU ++NuB/LqVcG/75JnDvVGImvoykCBEl9xASNOj1C4HvBwNrU7iIpvRig1wwHK8/wqU +Dhq5433rh40YbOfN0PLZDhQNhf3MysP3ipTZkVdwIKhsvl/vibQ2VmluY2VudCBI +ZW5uZWJlcnQgPHZpbmNlbnQuaGVubmViZXJ0QGFueXdhcmUtdGVjaC5jb20+iGAE +ExECACAFAkV5mgcCGwMGCwkIBwMCBBUCCAMEFgIDAQIeAQIXgAAKCRCgctTQQ1jF +hH7uAJ9BFRNMuSpvSq//lEWLc6WOTMW70ACghjA0jtaMQUVjDU3RUOdGMRkqRZu0 +KVZpbmNlbnQgSGVubmViZXJ0IDx2aGVubmViZXJ0QGFwYWNoZS5vcmc+iGMEExEC +ACMCGwMGCwkIBwMCBBUCCAMEFgIDAQIeAQIXgAUCRXmahgIZAQAKCRCgctTQQ1jF +hLRSAJ9KgHLa9V4Q2k0NFiMpPuhfnsK9bQCeJbGjFdIgrzyIvYIUZUPQGww597+5 +Ag0ERXmZwhAIAJhskbWZaLuwdZ3aLqVimVu65bR2ve1U28dfsSVCKx0uYCl3YJkj +lF9P3BfMMPVdNRqDz4Agz/Vrn13j+p4ZQQhVhv6IGhE4p4T4RebMaZ/d30/6REls +DP3Luc9IrnJbA72jSeXt+vI3WysB/wuJJ/kb+3KtFa1NowwVVypHCiSmme0VCUaR +K6jlN0245IpR0IA4Q1VziRO7v1VufZAef64/2U2T/IthwPqDoThNj++9Zg5Tctrt +TL02Z++n2Nj9bQf9R+FhA3YhvHuf6OLAPE2emcM/0As+JwBHqcMrmybnrxEJg0Aq +4Q1k1Ka7beb35QJ7158rzL1PU1V2totLeW8AAwUH/1NJVnMD/p4op2kbJYMgKSqz +zZfypt60aWeImeJ2qZD7FAtsz6KQu2a5ZkcVmyCeuAo9Sj0IqxXyn8Z6bHuHXNe+ +aidjS+n5kc2Y+5RQ1oMGV+BzXQMPGOgh0ertbLaairT7mCljTEd4kUGxOIcfAh/q +Ie+p7Guvw6+T4K7hgfY8bAiVgzhZLWIFTQXpjU+91q6kTt4eIWjdCGdNQ/OBezGY +f5SCy0phzTmRqcu68nKaLznwUpI7SuSFSIeVemGz602KdFsLEHHbZE0KJgd6aHYu +ynK3sugKIpz/NXpT2vBsVkK6EP5xzWYOpqMGb8uQD8CT7loY81SK+rHBm7dicgiI +SQQYEQIACQUCRXmZwgIbDAAKCRCgctTQQ1jFhBc3AKCQ1X7oIVR8g7GvSGEUw6DE +HgEaUgCgkl30lcl9gGa9hqk4cuGYn1OTyks= +=XphF +-----END PGP PUBLIC KEY BLOCK----- + +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1.4.7 (OpenBSD) + +mQGiBEfrX/ARBADulGs9V7StbfK8CFfClihdh4lRpw2tktMuYiQJ/rIsNq4zghkT +UGxFU0eUC2ZVPYvwG6sXgZ5SSm6i8Ii79YdIIxUG7oNwXaY/e9rK+xX193xqmDgl +Vh5vm4LUtncHi9TZDSF+g1YU1DVHbSjTd2oQEHxbBppL0CLfaLv3U1qr2wCgxeGw +ZIwb6KD6yesRLx6vGDkDzAMEAL99p0m4sNnjZdKC25Rrt6NZ7CROXWs89/+dkewg +JCZiVEoAcdSa0z5d8+XaSzmqR8BJWM1PBPjG48eEdqOevwypd0F/U1mwZ42MvQEw +oBayUY+7/pptW7C5L3Fjk18drE0a8lIzt9VBGX5fe3GoXtZKuOzDv0hEuHyzR9q9 +JLXsA/sELFfrHf0tBfXAdE7pj/Mahalu17/GAyb0RHSvuOfoUvXSXZA91cFg8ycB +4x265NtYZk52M32wi5ePrYeSJIZ6vWRvPuWVAXDg5S6HCMjcXc6ElkgLcUt1NoPI +DmRXe4FIDZkGSYSXdLTUByu7+8fCuWQHCFG2sALdOyVlucij2LQoTWF4aW1pbGlh +biBCZXJnZXIgPG1heGJlcmdlckBhcGFjaGUub3JnPohmBBMRAgAmBQJH61/wAhsD +BQkFM4sABgsJCAcDAgQVAggDBBYCAwECHgECF4AACgkQ4+RAT8wxrpcs1wCeIAoI +B0MwZfWLV+wD3pt1wDkPXbsAnjuOsyZ7EeNOcpoqSteB17aPRGeLiEYEEBECAAYF +Akfrb00ACgkQByq3OugVkrx76ACgqJ8W64IhBYgBvp3dTDLS825gCQQAnjB6X5+4 +eW/0pQUpJcvolxbT9xvF +=bbQO +-----END PGP PUBLIC KEY BLOCK----- + +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1.2.6 (GNU/Linux) + +mI4ESHiTQAEEANXyMbT/YZLUQoG2e5bVl1OcZ4Ttpt+Uk7KfZd1vehgwkr7VOUT5 +Y5JZ74mS/XxdG3JoSI5AF96pAxEl/xplqnTXtR/ZG6IcK7BZPblpYf4l8zB+M9g0 +QbTphAAiYeWBLoDWNZW6jiicsQ27EZmGgotcJ855t1x3CbEey0KpmEeHACCrR/j3 +tChNYXhpbWlsaWFuIEJlcmdlciA8bWF4YmVyZ2VyQGFwYWNoZS5vcmc+iEYEEBEC +AAYFAkh5tUsACgkQ4+RAT8wxrpeT/gCgvLNl0Ygmq8vBSdu6QXRzrWzQqGoAmQEq +gEXNzkIENamBEmderAxauC8HiLYEEwECACAFAkh4lMECGwMGCwkIBwMCBBUCCAME +FgIDAQIeAQIXgAAKCRD5iaLlyTxXAJlABACYHTf8nSIv0wR57ApHmF2AKub6OKPs +KBzHP0/xQKpJ+75suk/0CNL7ve5RknHZ9f202DBQ1UOE2ibW6CbXokuhLPQHiWcD +h/LNjn/gIB8y8yIr64SNX2u6563ZKhs2p72BTAJRquWdIwjcg6sXeWbvY/B7AFeJ +14v8GKWL3YWxQbiOBEh4k0gBBADGhb6pch9zGC9pGDPEyOujwXK1Iq9V7k6ECBSs +uzANb8GCAA+xj+jna20Nphnckd7ZMkiNyd+GD2fyFGL6BvIMGQZrLcuOK0yCkZqN +SVZ/Tk98u0PaaFMBMh1XwdCgn5NKtzxBOZ2b2klBJlI3Rr4vBUqwe6/mytH96DfM +cP5KWwAg9S7R24ifBBgBAgAJBQJIeJNIAhsgAAoJEPmJouXJPFcAFl8D/i5vkJOc +O/x3ANfQE/tod4DVo1f5+oEULBZOrq03M3bBAQOqe41hvlmrLjr0gTcKAwBp70w4 +iu/YXdirZTx9eTRMhnMS7HRJE2Aeui2eE3adDQV0hl7MouOgdWidIAQC3H+sNwT2 +rvqbxAg5UkH8u6UUGuMmtbOteaHArPDrujg5uI4ESHiTVgEEANRz9ivIoA1WSH+4 +Sr8QhhFMn/SUit/o403tAhCkIRJszvzIfelTLQzv17jBgNiVsnxtOtY+dIJLFfWS +cJohYlTEnLjeZeeA8abQUMjXpuKc0jUxspaHes3ywB/dcTAUZcsKxLboVT/X9S6j +99ErdM4Map9KOVgAzPGZLNgxLcq9ACCUGpzdiJ8EGAECAAkFAkh4k1YCGwwACgkQ ++Ymi5ck8VwBjxAQAhdcrZG0Q4LIk0d1RewO5dGua4XjqwyyhYuzHjs2X3Bel62bW +oK46DpretsNxnP80m79leHQOJalTAW2jMU6KGdLvlGaaaaZ0aAcxzeRltBsBWwe8 +9TdzBiO/eMiU4XB3PW+cjsc/gN/T4+hcbculNFB9skt7ODV4JHu6EZdSLmO5AaIE +SHm3yBEEALKRSNJ7OcgSRHAEDGLj0KZuy2PxMoCtUl4dPkdnWlcGe57j6l6PE5aW +jK6lxKQ10i7J4KM5kOt4nudWCmxD4SzSiZx7O7md4PgLypzDG3nDvPImS0Yu3G+S +g1oZ5bksqjd77X6f/OuGUl3/gOpo6IejlaMy21ixeRPR/8ZMl/OjAKDLontnXlp0 +y39oOJx+tQ8iWIzzuQP+NOqKnaO3aHnRE+Balyw/TpBpX5NnufgCFr0Psejf2fQz +xTbPpny7CsmMbMJlPHXE+OZ8mSy8mdryUIFy8C9bwPe/6iSsv5rnZ6JWJ2oRc8qC +fI38oSepEHTtHz9PhgaN8RNCqVyomuzXZmfkrcIVitsq3KVFTw8HhBA1Y/mHFjME +AIW1VSa0Pxm2eHCCcuieqa8zVtg/dt3qO5bgyyR2qCvA5xuYLrnTRYiOkddw9MQx +Zz5ZB2IGlF2jAyja+zHZqZAO3NiepvZwjo9zhQKzacKP/w94C1R/gL1V54/7p80H +ySRD1JolwBjoYxdKFtDj5HoJ5rC3jTbWY3l1AQQMShJxiO0EGAECAA8FAkh5t8gC +GwIFCQHhM4AAUgkQ+Ymi5ck8VwBHIAQZEQIABgUCSHm3yAAKCRCmAuWryUxvA7AE +AJ9jajPUCTGt3apXlLI18VFpxB8yCACdHKZcZ1JoDN94BVI2Qyt5SJ7RKknmMQQA +k6cWvEftN5Q94VOMjPuFOcrOvnjDGJkh4GgpbfSCLBTkX/nqus9iOMNYjHLL5pUi +jWEfdXJUenPfo58avgTzvDtF1EGoTw8ykzbPUZ1+CmekCt1/NaVSSY8+8HxS2FiE +kZoXlf56ekQOjeoa01Uo+MLc3aLBnFjW1fWAQmocRfC5AaIESHm4zBEEANrn/MA4 +BglSMlASTsCEnVJZ7vQwZYuQfxxCiGsjFckBWY/D4Xm+RB0L4y5bOuqNeAc01sb0 ++2MHpBqE1Zeq0nLFfTBACKH5GUnWB6f4T5P613Yjo3kR17/WSb28FnSQww2sP9gW +oSDoTn1x6iNeXh2waXW9WJQoCZHUDfkT68OrAKC9nE6MeuSPd+PNhAfBcR3S6SOd +hQQAuTmpLKGqx2auUnwDAajM/RuDCS1lNCAku2tHpvqSdDb0xfyLnTNwbjHG/cDX +oZLx9CYEoNkxQmlyUfg91/64xXjskbs7aTRV4tERV24duQnNXrWvQ2bfJeYxWNrV +VqlSeGokL5EYe7AViMwi+uLmDVlHjtxASiWaqqxIVC3dLXEEAK0F7bdFvw3RnPGT +x+JJV/5+L8mbTFdfwvWVXuN8oDkgAu9PkCLcAuPD/ZYsaZDic8MkcMbo5dmbH9+j +Qr8pft0pWJjueAj77zmETGKohqsf6LGRc10FAFF1E9zPFS8hvqdI9TfVBlxdPjwi +pNisGmlYsIUBqnHvrQva5L0sEofziO0EGAECAA8FAkh5uMwCGwIFCQHhM4AAUgkQ ++Ymi5ck8VwBHIAQZEQIABgUCSHm4zAAKCRCOHjXGZ1Q1G9DZAKC9K0qtWQE6CVic +IV659tdM0rZzqgCfaOC25bYAp4ISZBo+HUH6zJy51BDMzAP/TyqD7Fme1lwF2dWT +tWcUInqOwhLX896WaH9Y5NJYe98MNpQxr3wizYOwdJa3t9+MVvH0+s6ih4aaJ49n +71yaef7YVkZ+0gbdKrOOrILwPg4hHizMKKDGbMaJ1OTJbE3PjFhEsdZRy1M1wjzX +Jysr6rop4tOTfXLTX7m5hXkfJ/k= +=soRW +-----END PGP PUBLIC KEY BLOCK----- + +pub 1024D/5F298824 2006-09-30 [expires: 2011-09-29] +uid Simon Pepping +sub 2048g/40F32100 2006-09-30 [expires: 2011-09-29] + +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1.4.10 (GNU/Linux) + +mQGiBEUeNZ4RBADSETFxXTVUP98ydSQKh6gEkS59JPaCAkeNbMeM5VBK6RcVvjtK +zxadtIfcaFx++OCkABCdWnlIIVhjYMMYVV9h3DRFVwYEp1LZb9ktU+PxIw9kI75+ +eNMsxCpvYBvw9nFyHFPAdtnQPNpMSG7SeCNOHtSuErACuxOuo9JpwIWNwwCg469m +S/sca2kkmYuKqrWXD2cfZdED/3awMvn3cThlTsT0zqtQjuGBJjH9bJwiRdvNpU0M +cxw7xC1HGtfBYVQd9XastVj1xToDq7gCY5+qoHXLgv5BqLwecadtjuKEF4FXAmtN +1qmz9sAVlYsnA4azCADyLv0A440Azbzg9ruEBAf1U6JL6BtpKuLQ3RSCrYicO3Vu +fHJWA/0crJgn3GTDtFegMTDtlLvBcRXbsKBVAddbew9WOVfcTo1x7r71iziJZtgj +o88pNLHy6IXWvZr+VvB4c0V5U0K7eB9B956G3/t+1G8lDfuzVBqkSqAQXv7XmIWB +r4FIBMW20uNl8LO3461u+lDpIIXHZrucPmpHxrcFrzVTxknWqLQmU2ltb24gUGVw +cGluZyA8c3BlcHBpbmdAbGV2ZXJrcnVpZC5ldT6IaQQTEQIAKQIbAwUJCWYBgAYL +CQgHAwIEFQIIAwQWAgMBAh4BAheABQJFHkuQAhkBAAoJED4qb8JfKYgkvcAAnjXG +Sg3CFkDVdHQNA8pURERLc0dkAKCx583xMH1Mj6Bicvu2Y6YBKBl03ohMBBMRAgAM +BQJFIm1bBYMJYcnDAAoJEPcwlXB8YRWE5ykAoJHj4TgPp7PrHQBmvcU+6iUN9Iba +AJ9yP6eQoewU5K6z3fgH533MeNJZn4hmBBMRAgAmBQJFHjWeAhsDBQkJZgGABgsJ +CAcDAgQVAggDBBYCAwECHgECF4AACgkQPipvwl8piCSi+wCfbyubA0KeYeq81I21 +8k8EZBeh8hAAn0jfjHGnPRjLkTMntXKn5ecBMHl+iQEiBBABAgAMBQJFHkebBQMA +EnUAAAoJEJcQuJvKV618iBYH/izt6qzYzon3wLNQWdsNpPE31we5HQK8dV1Q9n+Y +eVBkPOvCJdFmIpWZ0GPzIa5c3Z5BUJ/Jl218eOOosmSme3I4Z2iWRvXTCzwxM7Ns +AR07jsRtnpf/hwyyQsLM/CXC+XQ868/2CJ/a9BomBbweQ4uLX9YMfJaZfty+TN8p +2Iw4ScluvM5zghYC9OYRNPtxGGpWwxiiz7XGxLvcDMzGvpq7Z7BT/3D895BAc15Z +LOLUTgGmPJResjojlPuf8cN5IgzKbr3jb67MwFwa+p0kwMX0RxSmFSu1PciyXgsI +vLHqIKq97SX226VReoojLXVlgH4692BAc8NDLmuhOufxCU6IRgQQEQIABgUCRXmd +sAAKCRCgctTQQ1jFhI/HAKCBpbQJlBCg6c5pCpQA0pbWJj4H7wCdEn4Tp6cOL4om +Mrn+WcndhYQV1VmInAQQAQIABgUCRjoJCwAKCRCazTzAqZ913b/cBADPHk651AFC +fjEyV9ag281WhylVErEOEbnKuftRGLQmrR7DwPqbESVMBbdfPOQGcTCtaL518W0J +A1nyF+0CR+VF5Ca+RVR7v5eN+EXaUYkSvEpGWGRizLZ4kXPC34MVV1+HWTO9hH0W +4i/9GYeOmtN1h+EkObhKvPpQsKyQcKuDnIkBIgQQAQIADAUCRjUVOgUDABJ1AAAK +CRCXELibyletfAVHCAC2d80OSi18/Q8eohrF7GGz3LpOs5pJlV03t8Ym0gpeUj36 +8sH14vtIcy4Ris809osV4Qwb1rHSdesTXcEmbXqp+DyGbny1FK0AqASWU7jx90iC +eR17kHKQzKfXQzO8GLic8nM4baQoJc/G1hhNYHhv6e1KR7jjurIqETFsP1ilAjBz +cK7WvPIHTKVNJtPE/JVPMoE20oM0AhbVTmqNuIezVar2LRnHq93ug3SO+p0/CJ2n +KnJy5uXI9dPwmEkwoHvlfYygcCMlFMyWJdbVtX9Oip1LWCcmk0V3Gyzajx6gQ3Gp +OT4Gm6I8DmdJ729PexLzB6REqseGewsbZGVbcKF5iEYEEBECAAYFAkY45HkACgkQ +FUWz/uIi3k/4hQCdEveDkhYCwUZANCdt7/YnNzrLd8cAniY3rgKN5iADh079Y7BV +pz0DGDNniEYEEBECAAYFAkY46RQACgkQY9CtrpESA+RUMQCffDDmM06WKNy6UtK9 +qZ1vuqxdaHQAnRi1+tt0C8AVxHLmrc8xJ4riWi8yiEYEEBECAAYFAkY6CQsACgkQ +UI6uxTAtpWgkPwCglCV6zLcE6W5eWHDc5iEe9ZbmOuwAn146k5PkdrdDbZmxOjnS +5K5nPoWyiEYEEBECAAYFAkY6CQsACgkQ3bpkuiwxLS8kPwCfWLpuiJrau1qcSBG5 +NXykWDOr0gIAn3aJCWmN+CGA4r34jYNRGtacXWlhiEYEEBECAAYFAkY8ymYACgkQ +mHDv8/EvYHJe0wCgkB0m7in5++2gZcEmdttG45hfTj0AoI/ii53ITSPefqHmJQzB +bals0Yu7iEYEExECAAYFAkY5GTYACgkQ4eHysJkO1Kq8GACeN5/Y3yzwehxoJGZh +0JKOg8hvwYQAn3ovqhb6frS9LpofciPlQqMvXSWFiEwEExECAAwFAkY8T/EFgwhH +5y0ACgkQTAQoGDEaPeXlSgCgtmGBnRIe8S5K3IpqlcfCAPdPxvUAoJGBgJHXg1Im +UshwAbwteZkdQVGyiEYEExECAAYFAkY9pHAACgkQLrlGgoiBdAJNogCdE7iG/eKK +O0VEFOSqQDYQ6Sng3MwAoIRMfBXZxir7htM/oidQDFkz7SJaiEYEEBECAAYFAkY+ +FAgACgkQAqWmBQt+bPrp7wCbBFHuKLDk6eRA0W3gdvyif6hJfVsAoKxSa6CAW8g5 +WNR952HrF8ppI8MciEYEEBECAAYFAkY+2+wACgkQVCINLMh0FVyfjwCeNCOwfFse +NsdCgcshIX1bY3l4h6IAoIP3umLxOhE/yV5iS6FQHwa30zT5iEYEEBECAAYFAkY4 +++EACgkQc92MFgFTAjX0lwCght491w1RhqKIwrav3K/fXqzKr5QAnRpvWtad3/2X +00mgOIpCrvKY/B/kiEYEEBECAAYFAkZGEngACgkQohFa4V9ri3JOogCdHeVASK7A +nKQRtmieDzqKl1lChgIAoNqpkJ3VEyl/WvoFW9An/lHSzIC4iHEEEBECADEFAkZI +xWEqHEhlbm5pbmcgU2NobWllZGVoYXVzZW4gPGhwc0BpbnRlcm1ldGEuZGU+AAoJ +EDKGTkGchSIrOG8AnROvc1A0G8kgCdUWkJMC2XSwMmh9AJ46ANKYC8natSxqozxM +gW7pOccPJoipBBARAgBpBQJGU8qlIBxTYW5kZXIgVGVtbWUgPHNhbmRlckB0ZW1t +ZS5uZXQ+IhxTYW5kZXIgVGVtbWUgPHNjdGVtbWVAYXBhY2hlLm9yZz4eHFNhbmRl +ciBUZW1tZSA8c2FuZGVyQG1hYy5jb20+AAoJELK+vEAVKSSvaB0AoNZvVKqr44P5 ++N2O67pg9raBEK1AAKDVfaF3I88KgwoxhVT9GI2UzjVB3YhGBBARAgAGBQJGPypG +AAoJEPXCYBZM7tdfs9UAniR2tRh8h7s7RflPT6lYYm5qzFWEAJ91ffpU1A53yRVR ++JhZejmuCarVN4hGBBARAgAGBQJGQL6SAAoJEG0LxzpAWBg3TucAn3Ae4QnaEqhe +KEHP/equWOPxnWmrAJwLv0enRtVE1Db59OzRs0CHmf2cRYhGBBARAgAGBQJGTEbA +AAoJEB8hI8Nr2HKgjukAn2gKtl0NF1OBdA8xqneWJPIxVircAKDRQZjglFLJLBez +JQz8MnGJ/G7UaohGBBARAgAGBQJGTIJCAAoJEA9FCiZiEL/Ao3AAoMOpPKpPGxJf +1KvbnEsH8v3CQN+zAJ9uUKEoBiGUxisZZqHXXd4Xf9G19IhGBBARAgAGBQJGUne3 +AAoJEDLB1u8PFDvBQycAni+fIPBFC36EF50yne0E0rXosVeiAJ9REycXBtugmf+5 +MDRR+fCgveyYYIhGBBARAgAGBQJGUnfBAAoJEMuuvjmkbEyhT4IAoNqs2oGq9pSa +kOh1jRMjbBcs45+WAJwPrhkv5v4d1AXR/BHkFv7YAOE0nYhGBBARAgAGBQJGXt33 +AAoJEJqG18zRqupg020AoOpOpYtaGLZtLeKRFT1Cxv7jrIKgAJkBy1XCk3eyk0l6 +JPhcTMjeF2YEfohGBBARAgAGBQJGXt5SAAoJEJqG18zRqupgMMIAoNivpEP5OOfq +ynsSlqy5mpZu5/NJAKDZZEYMVmnlbYIG+lq8beNIoOYej4hGBBARAgAGBQJGXt5b +AAoJEJqG18zRqupgQMIAn0n53SiM2QT+Pyh0Abg/8N8th67HAKDvh31p6Nbs+Go+ +E34KkJhystH2EIkBIgQQAQIADAUCRmpbBQUDABJ1AAAKCRCXELibyletfMatB/0Q +5EsLwryhcjAQUWrxisUl7Fx3JFv9txAr85yRirO6EmtjhfTjM1rCYVAQZ+G82PyA +EWE5hAGqXAE75y2+o/PJZJTSsB2nCjie0cLhsuWh+epRcC7t8Q3jevV0Y0uobAP7 +1q7jFIAaE5hSHOC4glfCVv5R9plHJBxGJ+AA+kEf6OSXmakvBTRybI7dNltaxs6s +z+XwfxOaPwHOcKDtOkIgF9zWPdWaNln4CYW50QRo4u0GTUTJncUqHF3RQE3mRF3s +K+FDhtDf1biXdy+6B0gROchvjiek6kZxBTsoKdyqsFyl+rRUloEQ9d8Kr26VRCZM +KPjAav8ZKI5aI77IxFt4iEYEEBECAAYFAkZ0E18ACgkQ1TNOdbExPeJFVwCgpGSO +XyEXyod1fLU3qZ2ZYU+lCvAAn1KbMu+gtTrDtkw9Rsw5cZ9AJttBiEYEEBECAAYF +AkalUscACgkQOb5RoQhMkRP87QCfb2LvY177HP7ggfvxFJjgMoAieuYAn1kCosw3 +DKlTqIe+gyc4P6X+iWmviGsEEBECACsFAkmMmbUFgwHihQAeGmh0dHA6Ly93d3cu +Y2FjZXJ0Lm9yZy9jcHMucGhwAAoJENK7DQFl0P1YPjoAn1eJPt58UwBXT8tR5qm6 +rHY0P2UTAJ9NOUtxQeEestgMuHjToiguxhP35IhGBBARAgAGBQJGrdMCAAoJEDPN +ZzOvXsRSgQAAoIBihP4nHW05Og6dyCFV/eFYoOoRAJ4nA6H9GTZM6+tt8H2OlfOz +aR9W/4kBIgQQAQIADAUCRzgQ1gUDABJ1AAAKCRCXELibyletfDF0B/90XPtQD4tu +MovOM++nGgpk7MINP+K1eOIyidGFD5NWHdJGOwvsP+NoNvXZxsO2+SXF/1ucnR+v +RWmTSa8VumiKN/zcfMwd8e6tzHw3vQUlQCkW9RYtxHNqtff6IEJSQSb+d7hL6WbK +RuGckVm7jJLTY4mdBvgbBXJ9dyYgAe0I3tptGx/KoChT4mBZkUofNLefs99ODdEP +rqchJ+tQ9yLeEXA4wIHQUooZUVueQgllymTv1QLg4phLwAsyRuvzkOBVIjO9uzx9 +XNc3cN+Zodqti4aoRFwpxGDfkzSXw1HW+P9+dSqnhwfHj21dHGcW0TJdSZILINO0 +nHsO+slRqoWdiQEiBBABAgAMBQJIEj/lBQMAEnUAAAoJEJcQuJvKV618CEAIAJ8M ++wBDED0VlEJFVNYJQOLS0TKwThI7LY73vgU0XyaGh0lsNaLJSfDktdzweo02UhCw +DgnnYFBQfitV5jLBHKHJsmHLFC7vLLngbnKmYG63bjRC6PTfNBCbOuaDyPEXxgAR +5g4z0tjme7t/wVAQgxKUJTfhAoXMK76y0RMlc0OjHCPs/L6Xn+jJawt7tKdIC5BY +Rzy2R7Y7zas1vYhGjl7vlU44+jplAXSbmuXtdnRovvkxxc3x39Ej8CzPrSTeoa/I +nWB55+C0Ud3zh7dsSmKXIU75ho/wOuDZbW+1l1pb9g5Tu6/lhGXlVs00bh1EjLhT +MHg2zQcFVkMQzcVBsWu5Ag0ERR41qRAIAIhe8b/RLEnK4T9cbL4QobABV2NsnTPP +2DJFOs5YKGxz+7MjptVSvGVAbBoSqxsIjvSovlaTsKcXXGUK618xrbxhdIm2U/OO +9wAA8Mk7SSZdYnQxhgPeuh3t+qNuKlbZPldDhRn7+fcoOdXY+iGwGeacY5drfScL +z6ALiahvNugiHyYFeqZk5wZbbYv/mJJZb5BTEeOcKU4JzU0VbCoui7KGmEBSM+H4 +z4uyx0zCgmTz4tcPwi6uJ0fOUxtThaR/67YpYmTsEvf9deX3LUhJ8MBkn2/N+zqK +UaYqZpS+YzJKjGq66Nmt21r7HjYZnXjN3CsgZQtEtnQK/+xVywbzdpMAAwUH/iag +f9dLI9A75pbMgdw+t95nNDOjRAvfQwpIpkmqgkAlqtrV1nsuUsi8IJ3W9ZH5QIk/ +LoWZ4gIDJ4ZtOmHzifurv0Ftvvp7MeoM5/Momu60wZfIeZloyy9RfymHCyr/A8VE +/ayEZQnpo76HiyhjsmcIQIvkiK8gzGnLpQPchcwNTM3CiZ2Wc9e2wlJtp1/r60uz +Lmv3m5iMms241FOfmxn0BnsVfFzEOG0uR5fQUXuanFAa7aGPxGticGi22mJkYhWO +RLrKoDYnnccpp8cN3shNKZVLYrGAiLPtM2p83I830BV5rTET5oCM948ZtNxkXnWP +ZAiIYSaFU+YapzevSpCITwQYEQIADwUCRR41qQIbDAUJCWYBgAAKCRA+Km/CXymI +JCaWAKCbFrAAM1xAOda4Wkn+NodF2EMosACeOEChn8Oeu4bPGP4DuLJgQH0jpSQ= +=geh1 +-----END PGP PUBLIC KEY BLOCK----- +pub 2048R/0ED3D317 2012-04-09 [expires: 2017-04-08] +uid Glenn Adams +sig 3 0ED3D317 2012-04-09 Glenn Adams +sub 2048R/F23E86C1 2012-04-09 [expires: 2017-04-08] +sig 0ED3D317 2012-04-09 Glenn Adams + +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1.4.11 (Darwin) + +mQENBE+DFvEBCACwHesM5hkZrlgPiq588FbZPe7QGtDRVdh0YbgI2A4Ky2odbFmA +/n7lF9v+yf+FUn/0H2igwDYqLFx4JsB7z+Nvj+hwExdGQxuDaGSwMjjpJeEiuEmf +b1Er5KYgpeFNm1MVP+r5kW5962O+HGIEx1xQ85AskNbr/jQkAg/wbbwWLFBQRMER +PqFs1rXL2aWip8XtNPKnKBLPlFPn9a7glgB5Whgbls44KpR0dplAVoCWMnZEYnTA +JYITcaDTCGFQI7aYgbj986RpS80o/Wcsnb8rjHKRZG8TaDr9MbgmmK39KwiJCflJ +9LvXR9g1j3addxgqEUuQBEtoLkWs/eDT/KQ7ABEBAAG0H0dsZW5uIEFkYW1zIDxn +YWRhbXNAYXBhY2hlLm9yZz6JAT4EEwECACgFAk+DFvECGwMFCQlmAYAGCwkIBwMC +BhUIAgkKCwQWAgMBAh4BAheAAAoJEGZUur4O09MXl7MH/jfeyE012/ABTTjvwvRw +2evD3cfghrZOQVT3PVEcVnXIcnVlmznO2DDs8aWXlhJ94ZnNdw8iKE8weK1FGXdd +qWA7vHrOI1UAo7h+CDaBt0TM6OEM3Mp3+5baOJMRXyrjhbv74sj88HYRtoPhA2+z +5ilVNmmX4PEAc6wqt87ZfNQ2ZqX2YzR+OaT5OggSB12qU/uE7d4d0NcDozEKQFUP +CMAuPJRNd4/J5yFy0DStfgsDKuUTir0oldlUElAjbhF8WtAgRwufIYuG/r4Cbuid +Qc32yVAoy069ZcggwBpkzVPOpL2by1fzeSohjIkntDIL474KuSSoBKCuw2nYxpyw +b0O5AQ0ET4MW8QEIAMFUAScm2Dw1fmBhmKMVc0mzqpR8TvSQvwJLiXRSP86NI1zc +DtuiXCFfNZykOifFk50otMH1hd91hqeQ6HfsxxYtXLg2UDHG2gTKzlWefwA+1eFc +M8fz/oV0CanGjqaOd6VZoYeg+LSD6YgrJZVjuoxNN/S0A9fNd8B3CXsG+92udkkW +YMyiUpD5qjMEwcjmj6ITX3M9VLxDCKKhYwN+u5wokuAk3yKtvBpWhJDQbaJcuDaC +rLH788gFybZDzyJ8Ug/95peV9Z57OCjMJLtn67ES7fB544EQRYVmDGnX5tTo9sYj +0bDjlbKX9lqS1sCGSezPxYxGgeXlnQrt+Sm+gd0AEQEAAYkBJQQYAQIADwUCT4MW +8QIbDAUJCWYBgAAKCRBmVLq+DtPTF2YbB/0Vo8Xr9N1adqlY1UAofvo5AoXZFxFJ +jkB0Eb3z1V3iSBh8P921LD0gUQz6JXptXZPVGnwgm/44RdqXC+kX+4MxiVBtNAHp +0ODjIXiabBrixAHnT7EcAJkWue0RP2J5wiYKklwWFbEetjl0QSmitz8Jh33eA76j +kFYW+vgKUMA8UrJ6j2Hk+vMFi0Odtntbc7AXvLifSPSmSztNe/Mws69EbuBph6bQ ++zSmTQ6nt8PX+v3e0PnkXdGD8si9H6hj0K32pEMz86ttSnAtrhvsAdKPYOkY7/JD +0Bl+bnb7FLxJjJGlGuO/Rn5SIR3fFNQGrE/zl2wsKtftUXse6kkZSue+ +=XDBB +-----END PGP PUBLIC KEY BLOCK----- +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1 + +mQINBFPO06wBEADhHglUaaLI6Nk+CUkpKMRWtVScsljRTmS16wxTdJ22kHgzkVYq +jomMDI2jAs0Bx//ju8Khsn3zZ/ipPVPgr9Wg3cwmnrmBDUP1qT34t0UQwBzot6PZ +YHY2sGvCaIPbuh8ZOIbT7u73ir9L+pNl+BmeCPDGV7H++71HxvCAW+2fEjwo+14K +Vua7HR/cwDa7DqnWXCLON7XMJmrzCr2YcKhyF9qAcMOyu/Y67yXEmlest6L/Nybm +tuY8g9EBz3iyQ7Ahc6KPRw9uWTRwuUhQlEDRnqOgUGGwijBj6dArLCkK5Y4Uq/2w +1MbWzjMcNVQO3nZJIhatczcvBWjR0MfdW5Px+lpdVanOTNPYz3lNsOcVr3UQEvJR +T8sPB3qTRu/inQJ2HJwzh490z/FnCfGepxB+36RaEjy06vBTZYWH77adg1eklUQF +h4pQq/mbEZvL/g1O3bWlUnWw/Xz1oBO/K8yqaryEEdTjcEVDXrvz3xDRSmXmZiei +hcSbNuvEvZI8l4Pe3dDwkSeJxKpaIGgZfPZH833Uder3R8rO+WJynpNKdHChrDTN ++y3CqOVNtmdd9Cf3mFS91U89ul0wqrGyESV0180FsRBMLTQX6xsGARpGZg+DW4DB +MhidUJbTOT7C0LYK39mimgFlzM+oN0tfU7tfYsNWzjsPJ5Rni/KGc9s8vQARAQAB +tClWaW5jZW50IEhlbm5lYmVydCA8dmhlbm5lYmVydEBhcGFjaGUub3JnPokCOAQT +AQIAIgUCU87TrAIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQ720xx3L6 +J1p8eg/+MkA0oWfP3SV47QtVxXSywCTGn1oN9R1k+iw+FHFitJ2u3dfZpVWbySSv +o49OhlR5YoQjA5aR4hXpicSmu0DJyBbZQBHjzllZC8CcU810uwT5E7qMD6rKviwk +cKM+q2ntI0yfZ0wI8rHWtTIV9FV0Y3YNuHoIPP86gyDTmNOMX5lR5EXGPYLZdD6e +NTMzu5VlQoW11AgXx5N+2qbRVD07v9nJpbl8c76/9A4eahVSXV9nhFsP2PFuEsDu +/EbszLALevu/ieE2delBpXcOErZsn4Tkg56pebOS4kjitERvzb5axAskulhQVVF0 +DTUOq21d77pD7Al6qRHODFH3hPGjCsk/2h8yVs1SK/IKRHR7LE5IP9EfVazJmOZa +/7DQ1my5WGD61/i2qa4hXYmDVTNEUWfJ5eSZ9Newm8pwa4pAVx2umOLe0xqxbUnk +SzbZGXKREIFOiDh0XA9VN7yNUWAFs6T49NsCHzn6q138I2Pp8hVL8Q142ywu5OLH +RDNTYfJZ0qLV/UyQv53g0zss2OkJz/r9WL7feWNtj15N3jJj+SCtj4f7PsJ5O7GX +LFWBV7HrXhfNQxwaRoE6ksDshW/AVIJ7mwNu4tXmXksFYd32VVHoHL+ppOqRo9aV +iN5aRt6CJFWYk4CU47rm7Vfgp9RRCXJyen8vM+KOe8gfIMG7g/OIRgQQEQIABgUC +U87YHgAKCRCgctTQQ1jFhA3gAJ9P8I4e6oTZrE7MPF0f+VlxW06BvQCfect5XgkZ +SjzjS7htUjNUApEcbN25Ag0EU87TrAEQAJ34nOX/79PUAl8kAyyPl/3V2My0DxnB +gyXwYne47fQxwYne1EWljO90KGRKiJoarClUZ3/QkD0jArGRgLKK2kEAF1RIkLdz +xNYE/weo7vc2rjRePSsGieE1Jy5aZHZkejaD95d/8Ve39qZqc17wWwF1ccw2OFlZ +e0Z4rwLo4f1nNrRoI+RHeB/2vdwYGdQRWQ8IQVCkEZs41wFh9XUVuM/SpY7+Tet7 +pL/NsSy0YmyXae0mHMOn1pXq0/K0zXZGy54EDD2flvyZhai+t+I/+R0xElrFsBDo +gt291s9YGPLJaQOHKXS9vxwUp7nj64rkE4fo5ogCSP+4WReZRfJaz+8tee3NE1zM +JHuc7oolZKUpxJlBmKrFs9qZCvAnpkrBcIZgUwiEqgZP8ptGXvcle9fqcIhpvqDo +NRyW98cGQZOe9QwV9tCnBtSFMnoUFUXuvfy3stEBBWLYSWABip98W75TO8tVn8DG +SX3ApIpIIBhUmnDR3RWN9N/5AFTiDLr/Ux+mUBM7u7vTImMvuQF8+UNxXCg//rmg +vtYtMQ8Jv1k6V7RPbjEbkRXNt+JIpxSy5rQo/USEJxiwoiQKq5blGrBxURWS4jQs +Vc/Mhh9OUdDftqyH4ZHgNbbzgTu9y9CCloD4N0gzGTjqPFvLpKMuiv3IKKlHe9wC +Vtjq2eShQOxvABEBAAGJAh8EGAECAAkFAlPO06wCGwwACgkQ720xx3L6J1qAzw/+ +PQ1gpXiNz4+6GJqDWhtmzbPpg5XVUMkO1fCyaqdJu7QmQhZQo3ZNtqqq7zm2PuA8 +/HJEzbzq6s7jB+qiR7lsLIzqiXVTe0kBpjva8PbPf9m8o7sm79hDwIhxuP0vac+6 +bpdpbzuEQ9VczVVgz1UruHhfA2Jgd67D9gXykQCNWWTewZ6MX0bWUQFkI0kIcFSq +QqPMoHBm1LNef6sUg+bCDn1lGiXZZYeMkbeMZ7zlsqERBijFGfAatyjUk/I/W/R6 +o9ZKHsWJTLYg9oKTzwYYErHXoDm0wYehGC8vhMlc4QpCx0HrLBl2w+br7AErfKO9 +5g/YF1lotpsgFm8gswmED0+8qM9xm7JbJah3ahv6anttk7FjUOxgcclMNf+8ZW8l +oKnOev7LmF9pBLBZ2YYZPl/GOU/x5aRqU12qmxeHwPp1hZ0C7kngkhtlR/WGuWp3 +FfAihujJYCm/OLL/sfZYJcC1W1w5Q/tJsoVlwCuAJLifpHxvNcScdLYLfSVFX7L+ +WZd4RVs2TDptk5ovcQk0GnlMK090aGE8nV3+oV72wssjWlMycKfQnCN2fP/DvHic +C7ntv74LfH+9sIY9lrW3mqPSUqBpEkVQvWfO5yaABDyKWQKbL0oTIOqwDSqOw0Ll +Jb8/mPX9jDboY95OyNYZ8D7o+lwQFeUbNwQp/qCS52g= +=zlG2 +-----END PGP PUBLIC KEY BLOCK----- +pub 4096R/7CDB6DEA 2015-05-20 +uid Simon Steiner (CODE SIGNING KEY) +sub 4096R/73382B77 2015-05-20 + +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1 + +mQINBFVcZesBEAC5Poew53ijRTE5kV2o2Tj+plJmoQMRtfiurMUpPQ4s/vTz8Bte +WoHdNfJrzordHBZdy1S7V8p2kTtKWyaNFMyVoavBqo/BurmJ/3qUKH5otXkXuaAi +zFaJPeEnR6y7pxTYkwFKyrn7TE4I84JKZEMLEbJIw1YF3t76nj055/OnzRtwc2Df +cPyGqDALyoFXk6R04qIotVmlX3NwwF6A0M79+W+kWvPg4sD4R6SqwJb1UhcqHrqb +kQOllg74cBjyAikRbqrCfoydHM10gc8f5w9+8v/fbDkSBtoh8Y31WSK6ocoWIg85 +I3odqHFxThwFTAYQC2tdw1M5kYcOiYVqw91JvBivpKH02vryH2BJu9zQDNFeyXud +wyengMLlzUUmf9jBk3M8IUE4JTerw1AzWj0b3jdvG7FrgHrlszb6eCzE3BdY7zGS +Qh6e+ROsF2LEKkbjxeAg58nl/h5iLythheY0S6sspeOTuDtgLgm8TZiGHA+j/qKH +DY+aRReI0X2bzqNZROhTIXHSKsoX9+9g5a+1NYQeZmiICNo4vj+r4+i037QvIoy6 +3K4NSVQgKnR9bbgyQCUprifrkywKF5a8eP3wtUGtH2FNqFQg1rUKBgLjLbRJ8PF2 +Ag8O0jmJBPxdOIdVP4vz37m+jyKE6L4JWjnaFldtD3NDJI1QQYX4MmXEaQARAQAB +tDZTaW1vbiBTdGVpbmVyIChDT0RFIFNJR05JTkcgS0VZKSA8c3N0ZWluZXJAYXBh +Y2hlLm9yZz6JAjgEEwECACIFAlVcZesCGwMGCwkIBwMCBhUIAgkKCwQWAgMBAh4B +AheAAAoJEFuT8d98223qTtoP/j5AH1w90HOiYioGGgxrxOciQdGNht0yOVPXHDyj +8oKNBKYhNYQ9tAxNxEZFVS2KxB3R5dy/Q/9XvOP/NYTP8p0VyXmxNMkicUhJQOJ/ +6mnxkTY/jk7y2XkYtdCby28SfMdExbM501arnG+vmXRtr3TpRRAxXCfS3FBI2n1H +Y7aX8pVX3NNRQqGxjnSv4q/2NKrR1ZFt7aDuAO9VfLXd5NkJkaGJ7E7T4AQMJE6K +KYUFkYKGeOc5dm1mHZMTVanyYRS0qWSpcnsamdU76HQgtC7koJmaeeI0MtntMkpB +jObTY80iam97P81hfhEi9UsAPy0WPBmossNqQFYQ5kQzxq030C1ERJl1nXJCkIpl +CpVv/aIV+kM+WQeA41IHgfLZsxHTkYRxtCZXh8HbFhpyKGwNRZhyvPn0oSbjQcke +oAbfnxCj1YH6TQGJQH1R1rGPv3ph3omq9pRuJ/th4uFmq06xpO6VLjUICV9F1N1J +kmiVpivC5iRDKw30YeRFe/J0BpflPizQh2gnTFCg0Q0ICUdehGovWKPDkJsG2+hb +AJaxfYQg5cJQI1P0UOc3w6rLh2ye3qkSRxF1O9oyRHuAZtKtsabEg3v7bx6RnX1F +BYGCB8ckud+qYlBjiGKLP7fJotuZv3hFWlFBoXb9F0WDpx+gUoEUH8H+9ygodEUq +gjCRuQINBFVcZesBEADFcTVSAZoaax9xpw1zxwjVgfQrNfobqnJM33r2EBSB4b80 +NbPOtoHJ2OUvHq4w84PB+H4+JNAIA2KbPOvzB+u1Bn5IgJr9asH5lhrnrUSR+YCw +UlnCw+2YJumdv/l6RKcFKbSRZHWSwTD3ZLBMuPUqX9uZkOuG1ftTkcVtfFHN3WAu +ckHjVxTSkaHhZ7ALz8LIUH+8AL3/3eThM1KEgx+bcod8YttctzjTy9cAaT93DwKO +qzqHXpk6Fpv9bvdiNye8Ye7f8487N0/3alMPpJqt2tHAqjxJRDb590/09cbt2N6l +6B+WlpM5hgkV//7ViL357Hids64sqbYSnmpQjZWUdafDvVvMKYbITmx26BEoWtIq +wwpj7+DdQlxd/7bAtyk8XiHe6Ej9FF1PUEvBQw5NXOUiCSBo0hzcMMNy1uRb/Wpd +oZX3TFKXTWulv95uzKGAo3zrUuKcfOGfDgqZ/H2dy8Yw9DaWJRnXyYwweUi1Ptnc +Nk9ktycypCcVbsOQhGdQJNyGVUVgVV1CWz5GJ3eqtjHY09r7HEm6bGviJRu/sosk +XmmjRUFpqwtDCRACYShDpnFjpmWTZegPcwpN+492qLP6cjh1XSN4Cg1YZ/0BMY4N +V/yeadQf/hbM5hzPOhN6mr+VHOLulyXuV49EWisIRferqaXG67zIXbVQ4qfqfwAR +AQABiQIfBBgBAgAJBQJVXGXrAhsMAAoJEFuT8d98223qGKoP/RtyjtE5zEkrd7oa +vRK5FmNtdh8r63v8lwjxayCfmpQw14AQaPY7//ulfJQExpxqcCLFIEBqZ8o22RYA +XnsV6RF3JAIjSktAh57GMqUljeNxBtlix2zv5MVi/XiYcmQrID812KNfp3/guJfK +3Us5BOxLyDwHylwyb88dLM3rP4W8VvaRAEXfd0/6rhEugdCdcHD1c+E1XliqBw/T +UP3JjnnABfKR4NMZ67ENxc5jPDg2Jh3wychGGhvLddqk32KEIDTbV2SZYxtw3ylP +cRUtEoVGvkCDj8KpcLvvTbE7QCOycQMj1aHt5tc0w+Ibqbj3X+DdJEAxuZhMDyR6 +hDkIr7YGkRjFacYeaunoY4tX4+x4tkf33eNsBa3DV6r6R18DWh8o6bWzHpPuEMZT +ewbUuSHF9xm0Ti3uehDHNHIFsnEpGDanb2UMO1kd2Xr6kjabce4DIHn+PhOqwC1o +bO9KBKaWO4uR7K1eWjOkqn70F7ykGz01AveouwRyOjh59VdpVe/Zpgtp/IvD6bP2 +FbW6pZZeHWWoj+zaDmyRDn5wRfGkNuLUIMblgCXQXttAm/nYdRwPQyAI+T5CP8f+ +nmZfX1E6PwDD9rT+fU+0R1eAIkEZE+0UfWoBwbnei19Xqpwx6VH5x8mtfvUH9J5H +PYDEgpBqDxD3E1CYq/cVFhKQRxpK +=n+z/ +-----END PGP PUBLIC KEY BLOCK----- diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..c93ae11 --- /dev/null +++ b/NOTICE @@ -0,0 +1,5 @@ +Apache XML Graphics Commons +Copyright 2006-2022 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). diff --git a/README b/README new file mode 100644 index 0000000..90660d5 --- /dev/null +++ b/README @@ -0,0 +1,177 @@ +$Id: README 1896948 2022-01-12 10:37:20Z ssteiner $ + +============================================================================== + APACHE XML GRAPHICS COMMONS - README +============================================================================== + +Contents of this file: + - What is Apache XML Graphics Commons? + - Where to get help? + - How do I build Apache XML Graphics Commons? + - Legal information + - Release Notes + +============================================================================== + + +What is Apache XML Graphics Commons? +--------------------------------------- + +It is a place where the Apache Batik and Apache FOP share commonly used +components. Many components in here are also usable and useful stand-alone. + +Documentation +--------------------- + +The documentation can be found under: +http://xmlgraphics.apache.org/commons/ + +Where to get help? +--------------------- + +Please subscribe to the general@xmlgraphics.apache.org mailing list by sending +an empty mail to general-subscribe@xmlgraphics.apache.org. All you questions +about XML Graphics Commons will be answered there. + +Please report bugs to JIRA at http://issues.apache.org/jira/ (project +XMLGraphicsCommons). + + +How do I build Apache XML Graphics Commons? +---------------------------------------------- + +If you've downloaded a binary distribution of Apache FOP or Apache Batik, +you don't need to build XML Graphics Commons. It is distributed with these +two products. + +Otherwise, install Apache Ant (http://ant.apache.org) and run "ant" from the +command-line in the directory where you find "build.xml". + +Legal information +-------------------- + +Apache XML Graphics Commons is published under the Apache License +version 2.0. For the license text, please see the following files: +- LICENSE +- NOTICE + +Legal information on libraries used by Apache XML Graphics Commons, i.e. +its dependencies, can be found in the "lib/README.txt" file. + +Apache XML Graphics Commons contains unmodified copies of the Adobe Glyph +List and the ITC Zapf Dingbats Glyph List, available from: +http://www.adobe.com/devnet/opentype/archives/glyph.html + +License labeling (according to http://www.apache.org/legal/3party.html): +- Multi-Licensed +- Source Available +- No Reciprocity Required + +============================================================================== + KNOWN ISSUES +============================================================================== + +- PostScript output: When TexturePaint is used with PSGraphics2D, the output + violates the DSC specification. Extracting single pages from a multi-page + PostScript file can result in errors. + +============================================================================== + RELEASE NOTES +============================================================================== + +For more detailed info about the changes, please see: +http://xmlgraphics.apache.org/commons/changes.html + +Version 2.7 +-------------- + +Minor release to be used with FOP 2.7 + +Version 2.6 +-------------- + +Minor release to be used with FOP 2.6 + +Version 2.4 +-------------- + +Minor release to be used with FOP 2.4 + +Version 2.3 +-------------- + +Minor release to be used with FOP 2.3 + +Version 2.2 +-------------- + +* Transition from Ant to Maven Build Process + +Minor release to be used with FOP 2.2 + +Version 2.1 +-------------- + +Minor release to be used with FOP 2.1 + +Version 2.0.1 +-------------- + +Minor release to be used with FOP 2.0 + +Version 2.0 +-------------- + +The main new feature is the introduction of a URI resolution framework that +makes it easier to control resource access in a cloud environment. Because of +changes to the API, the version number has been bumped to 2.0. + +This release also contains a number of bug fixes. + +Version 1.5 +-------------- + +This release of Apache XML Graphics Commons primarily addresses bug fixes +and also adds a number of new features. + +Version 1.4 +----------- + +This release adds the option to generate smaller PostScript files, +support for the AdobeStandardCyrillic encoding, RefinedImageFlavor, +TexturePaint support for PSGraphics2D (PostScript tiling patterns), +improvements to the XMP framework, optimization for PostScript state +handling in (E)PSDocumentGraphics2D, and more. In addition it contains +a number of bug fixes. + +Version 1.3.1 +-------------- + +This release is mostly a bugfix release for the image loading framework that +has been introduced in version 1.3. + +Version 1.3 +-------------- + +The most important addition in this release is an image loading framework +which supports all sorts of different image formats (bitmap and vector) and +is highly extensible. +Besides that there were a larger number of smaller additions and bugfixes. +Support for Java 1.3 has been dropped. Java 1.4 or later is required now. + +Version 1.2 +-------------- + +This release mainly adds support for CMYK and GRAY color spaces for PSGenerator. + +Version 1.1 +-------------- + +This release adds an XMP metadata framework and brings improvements for the +ImageWriter package plus some minor fixes mainly in the PostScript area. + +Version 1.0 +-------------- + +This is the first release of Apache XML Graphics Commons. There are currently +no known issues with the code. diff --git a/build.properties b/build.properties new file mode 100644 index 0000000..077d6d6 --- /dev/null +++ b/build.properties @@ -0,0 +1,44 @@ +## This is a template for settings which are useful to be +## overridden in a developer specific property files. +## Copy this to build-local.properties, uncomment and change +## properties which should be overridden. +## The file buil-local.properties is not stored in the code +## repository and ignored for file adds. + +## =================================================================== +## 1. Path settings + +## Path to your IKVM installation. Set this if you want to compile +## XML Graphics Commons to a .NET DLL. +## NOTE: If you want to compile the .NET DLL you'll have to +## disable the internal codecs. See below. +#ikvm.dir=C:\\javalib\\ikvm-12-07-2004\\ikvm + +## =================================================================== +## 2. Switches for common tasks + +## Javac switches +# javac.debug = on +# javac.optimize = off +# javac.deprecation = on +# javac.source = 1.4 +# javac.target = 1.4 +# javac.fork = on + +## JUnit task switches +# junit.fork = on + +## Packages to produce javadoc. +# javadoc.packages = org.apache.xmlgraphics.* + +## =================================================================== +## 3. Project specific properties + +version = 2.7 + +## Allows you to switch off the compilation of the internal image +## codecs which depend on Sun-private classes. Setting this to true +## enabled compatibility with non-Sun Java implementations such as +## Kaffe, IKVM, GCC/GCJ or even Apache Harmony (i.e. system which +## are based on GNU Classpath, for example). +# internal-codecs.disabled = false diff --git a/build.xml b/build.xml new file mode 100644 index 0000000..25d44d4 --- /dev/null +++ b/build.xml @@ -0,0 +1,726 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +NOTE: +************************************************************************** +* One or more of the Junit tests had Failures or Errors or were skipped! * +* Please check the output above for relevant messages. * +************************************************************************** + + All Junit tests passed! + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/examples/java/image/loader/ImageViewer.java b/examples/java/image/loader/ImageViewer.java new file mode 100644 index 0000000..b2d746b --- /dev/null +++ b/examples/java/image/loader/ImageViewer.java @@ -0,0 +1,139 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageViewer.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package image.loader; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.geom.Rectangle2D; +import java.io.File; +import java.io.IOException; + +import org.apache.xmlgraphics.image.loader.ImageException; +import org.apache.xmlgraphics.image.loader.ImageFlavor; +import org.apache.xmlgraphics.image.loader.ImageInfo; +import org.apache.xmlgraphics.image.loader.ImageManager; +import org.apache.xmlgraphics.image.loader.ImageSessionContext; +import org.apache.xmlgraphics.image.loader.ImageSize; +import org.apache.xmlgraphics.image.loader.impl.DefaultImageContext; +import org.apache.xmlgraphics.image.loader.impl.DefaultImageSessionContext; +import org.apache.xmlgraphics.image.loader.impl.ImageGraphics2D; +import org.apache.xmlgraphics.java2d.Graphics2DImagePainter; + +/** + * Very simple image viewer application that demonstrates the use of the image loader framework. + */ +public class ImageViewer { + + private ImageManager imageManager; + + public ImageViewer() { + //The ImageManager is set up for the whole application + this.imageManager = new ImageManager(new DefaultImageContext()); + } + + public void display(File f) throws IOException { + //The ImageSessionContext might for each processing run + ImageSessionContext sessionContext = new DefaultImageSessionContext( + this.imageManager.getImageContext(), null); + + //Construct URI from filename + String uri = f.toURI().toASCIIString(); + + ImageGraphics2D g2dImage = null; + try { + //Preload image + ImageInfo info = this.imageManager.getImageInfo(uri, sessionContext); + + //Load image and request Graphics2D image + g2dImage = (ImageGraphics2D)this.imageManager.getImage( + info, ImageFlavor.GRAPHICS2D, sessionContext); + + } catch (ImageException e) { + e.printStackTrace(); + + //Create "error image" if the image cannot be displayed + g2dImage = createErrorImage(); + } + + //Display frame with image + ViewerFrame frame = new ViewerFrame(g2dImage); + frame.setVisible(true); + } + + private ImageGraphics2D createErrorImage() { + Graphics2DImagePainter painter = new Graphics2DImagePainter() { + + public Dimension getImageSize() { + return new Dimension(10, 10); + } + + public void paint(Graphics2D g2d, Rectangle2D area) { + g2d.translate(area.getX(), area.getY()); + double w = area.getWidth(); + double h = area.getHeight(); + + //Fit in paint area + Dimension imageSize = getImageSize(); + double sx = w / imageSize.getWidth(); + double sy = h / imageSize.getHeight(); + if (sx != 1.0 || sy != 1.0) { + g2d.scale(sx, sy); + } + + g2d.setColor(Color.RED); + g2d.setStroke(new BasicStroke(0)); + g2d.drawRect(0, 0, imageSize.width, imageSize.height); + g2d.drawLine(0, 0, imageSize.width, imageSize.height); + g2d.drawLine(0, imageSize.height, imageSize.width, 0); + } + + }; + Dimension dim = painter.getImageSize(); + + ImageSize size = new ImageSize(); + size.setSizeInMillipoints(dim.width, dim.height); + size.setResolution(imageManager.getImageContext().getSourceResolution()); + size.calcPixelsFromSize(); + + ImageInfo info = new ImageInfo(null, null); + info.setSize(size); + return new ImageGraphics2D(info, painter); + } + + /** + * The application's main method. + * @param args the command-line arguments + */ + public static void main(String[] args) { + try { + ImageViewer app = new ImageViewer(); + if (args.length < 1) { + throw new IllegalArgumentException("No filename given as application argument"); + } + app.display(new File(args[0])); + } catch (Throwable t) { + t.printStackTrace(); + System.exit(-1); + } + } + +} diff --git a/examples/java/image/loader/ViewerFrame.java b/examples/java/image/loader/ViewerFrame.java new file mode 100644 index 0000000..814893d --- /dev/null +++ b/examples/java/image/loader/ViewerFrame.java @@ -0,0 +1,94 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ViewerFrame.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package image.loader; + +import java.awt.Dimension; +import java.awt.Frame; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.awt.geom.Rectangle2D; + +import javax.swing.JPanel; + +import org.apache.xmlgraphics.image.loader.impl.ImageGraphics2D; +import org.apache.xmlgraphics.java2d.Graphics2DImagePainter; + +/** + * Viewer frame for the image viewer. + */ +public class ViewerFrame extends Frame { + + public static final String TITLE = "Very Simple Image Viewer"; + + public ViewerFrame(ImageGraphics2D g2dImage) { + super(TITLE); + addWindowListener(new WindowHandler()); + buildGUI(g2dImage); + setSize(500, 400); + } + + private void buildGUI(final ImageGraphics2D g2dImage) { + JPanel imagePanel = new JPanel() { + /** {@inheritDoc} */ + protected void paintComponent(Graphics graphics) { + super.paintComponent(graphics); + Graphics2D g2d = (Graphics2D)graphics.create(); + try { + Rectangle paintRect = new Rectangle( + 30, 30, + getWidth() - 60, getHeight() - 60); + //g2d.translate(paintRect.getX(), paintRect.getY()); + + Graphics2DImagePainter painter = g2dImage.getGraphics2DImagePainter(); + Dimension dim = painter.getImageSize(); + double sx = paintRect.getWidth() / dim.getWidth(); + double sy = paintRect.getHeight() / dim.getHeight(); + //g2d.scale(sx, sy); + + /* + Rectangle2D targetRect = new Rectangle2D.Double( + paintRect.x * sx, paintRect.y * sy, + dim.width, dim.height); + */ + Rectangle2D targetRect = new Rectangle2D.Double( + paintRect.x, paintRect.y, + paintRect.width, paintRect.height); + + + g2d.draw(targetRect); + painter.paint(g2d, targetRect); + } finally { + g2d.dispose(); + } + } + }; + add("Center", imagePanel); + } + + private class WindowHandler extends WindowAdapter { + public void windowClosing(WindowEvent we) { + System.exit(0); + } + } + +} diff --git a/examples/java/image/writer/ImageWriterExample1.java b/examples/java/image/writer/ImageWriterExample1.java new file mode 100644 index 0000000..48de461 --- /dev/null +++ b/examples/java/image/writer/ImageWriterExample1.java @@ -0,0 +1,148 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageWriterExample1.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package image.writer; + +import java.awt.Color; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.font.TextAttribute; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.text.AttributedString; + +import org.apache.commons.io.IOUtils; +import org.apache.xmlgraphics.image.writer.ImageWriter; +import org.apache.xmlgraphics.image.writer.ImageWriterParams; +import org.apache.xmlgraphics.image.writer.ImageWriterRegistry; + +public class ImageWriterExample1 { + + /** + * Paints a few things on a Graphics2D instance. + * @param g2d the Graphics2D instance + * @param pageNum a page number + */ + protected void paintSome(Graphics2D g2d, int pageNum) { + //Paint a bounding box + g2d.drawRect(0, 0, 400, 200); + + //A few rectangles rotated and with different color + Graphics2D copy = (Graphics2D)g2d.create(); + int c = 12; + for (int i = 0; i < c; i++) { + float f = ((i + 1) / (float)c); + Color col = new Color(0.0f, 1 - f, 0.0f); + copy.setColor(col); + copy.fillRect(70, 90, 50, 50); + copy.rotate(-2 * Math.PI / (double)c, 70, 90); + } + copy.dispose(); + + //Some text + copy = (Graphics2D)g2d.create(); + copy.rotate(-0.25); + copy.setColor(Color.RED); + copy.setFont(new Font("sans-serif", Font.PLAIN, 36)); + copy.drawString("Hello world!", 140, 140); + copy.setColor(Color.RED.darker()); + copy.setFont(new Font("serif", Font.PLAIN, 36)); + copy.drawString("Hello world!", 140, 180); + copy.dispose(); + + //Try attributed text + AttributedString aString = new AttributedString("This is attributed text."); + aString.addAttribute(TextAttribute.FAMILY, "SansSerif"); + aString.addAttribute(TextAttribute.FAMILY, "Serif", 8, 18); + aString.addAttribute(TextAttribute.FOREGROUND, Color.orange, 8, 18); + g2d.drawString(aString.getIterator(), 250, 170); + + g2d.drawString("Page: " + pageNum, 250, 190); + } + + /** + * Creates a bitmap file. We paint a few things on a bitmap and then save the bitmap using + * an ImageWriter. + * @param outputFile the target file + * @param format the target format (a MIME type, ex. "image/png") + * @throws IOException In case of an I/O error + */ + public void generateBitmapUsingJava2D(File outputFile, String format) + throws IOException { + //String compression = "CCITT T.6"; + String compression = "PackBits"; + boolean monochrome = compression.startsWith("CCITT"); //CCITT is for 1bit b/w only + + BufferedImage bimg; + if (monochrome) { + bimg = new BufferedImage(400, 200, BufferedImage.TYPE_BYTE_BINARY); + } else { + bimg = new BufferedImage(400, 200, BufferedImage.TYPE_INT_RGB); + } + + Graphics2D g2d = bimg.createGraphics(); + g2d.setBackground(Color.white); + g2d.clearRect(0, 0, 400, 200); + g2d.setColor(Color.black); + + //Paint something + paintSome(g2d, 1); + + OutputStream out = new java.io.FileOutputStream(outputFile); + out = new java.io.BufferedOutputStream(out); + try { + + ImageWriter writer = ImageWriterRegistry.getInstance().getWriterFor(format); + ImageWriterParams params = new ImageWriterParams(); + params.setCompressionMethod(compression); + params.setResolution(72); + writer.writeImage(bimg, out, params); + + } finally { + IOUtils.closeQuietly(out); + } + } + + /** + * Command-line interface + * @param args command-line arguments + */ + public static void main(String[] args) { + try { + File targetDir; + if (args.length >= 1) { + targetDir = new File(args[0]); + } else { + targetDir = new File("."); + } + if (!targetDir.exists()) { + System.err.println("Target Directory does not exist: " + targetDir); + } + File outputFile = new File(targetDir, "eps-example1.tif"); + ImageWriterExample1 app = new ImageWriterExample1(); + app.generateBitmapUsingJava2D(outputFile, "image/tiff"); + System.out.println("File written: " + outputFile.getCanonicalPath()); + } catch (Exception e) { + e.printStackTrace(); + } + } + +} diff --git a/examples/java/image/writer/ImageWriterExample2.java b/examples/java/image/writer/ImageWriterExample2.java new file mode 100644 index 0000000..45ee097 --- /dev/null +++ b/examples/java/image/writer/ImageWriterExample2.java @@ -0,0 +1,118 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageWriterExample2.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package image.writer; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; + +import org.apache.commons.io.IOUtils; +import org.apache.xmlgraphics.image.writer.ImageWriter; +import org.apache.xmlgraphics.image.writer.ImageWriterParams; +import org.apache.xmlgraphics.image.writer.ImageWriterRegistry; +import org.apache.xmlgraphics.image.writer.MultiImageWriter; + +public class ImageWriterExample2 extends ImageWriterExample1 { + + private BufferedImage createAnImage(String compression, int pageNum) { + boolean monochrome = compression.startsWith("CCITT"); //CCITT is for 1bit b/w only + + BufferedImage bimg; + if (monochrome) { + bimg = new BufferedImage(400, 200, BufferedImage.TYPE_BYTE_BINARY); + } else { + bimg = new BufferedImage(400, 200, BufferedImage.TYPE_INT_RGB); + } + + Graphics2D g2d = bimg.createGraphics(); + g2d.setBackground(Color.white); + g2d.clearRect(0, 0, 400, 200); + g2d.setColor(Color.black); + + //Paint something + paintSome(g2d, pageNum); + + return bimg; + } + + /** + * Creates a bitmap file. We paint a few things on a bitmap and then save the bitmap using + * an ImageWriter. + * @param outputFile the target file + * @param format the target format (a MIME type, ex. "image/png") + * @throws IOException In case of an I/O error + */ + public void generateBitmapUsingJava2D(File outputFile, String format) + throws IOException { + //String compression = "CCITT T.6"; + String compression = "PackBits"; + + OutputStream out = new java.io.FileOutputStream(outputFile); + out = new java.io.BufferedOutputStream(out); + try { + + ImageWriter writer = ImageWriterRegistry.getInstance().getWriterFor(format); + ImageWriterParams params = new ImageWriterParams(); + params.setCompressionMethod(compression); + params.setResolution(72); + + if (writer.supportsMultiImageWriter()) { + MultiImageWriter multiWriter = writer.createMultiImageWriter(out); + multiWriter.writeImage(createAnImage(compression, 1), params); + multiWriter.writeImage(createAnImage(compression, 2), params); + multiWriter.close(); + } else { + throw new UnsupportedOperationException("multi-page images not supported for " + + format); + } + + } finally { + IOUtils.closeQuietly(out); + } + } + + /** + * Command-line interface + * @param args command-line arguments + */ + public static void main(String[] args) { + try { + File targetDir; + if (args.length >= 1) { + targetDir = new File(args[0]); + } else { + targetDir = new File("."); + } + if (!targetDir.exists()) { + System.err.println("Target Directory does not exist: " + targetDir); + } + File outputFile = new File(targetDir, "eps-example2.tif"); + ImageWriterExample2 app = new ImageWriterExample2(); + app.generateBitmapUsingJava2D(outputFile, "image/tiff"); + System.out.println("File written: " + outputFile.getCanonicalPath()); + } catch (Exception e) { + e.printStackTrace(); + } + } + +} diff --git a/examples/java/java2d/ps/EPSColorsExample.java b/examples/java/java2d/ps/EPSColorsExample.java new file mode 100644 index 0000000..f28caf6 --- /dev/null +++ b/examples/java/java2d/ps/EPSColorsExample.java @@ -0,0 +1,125 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: EPSColorsExample.java 1051421 2010-12-21 08:54:25Z jeremias $ */ + +package java2d.ps; + +import java.awt.Color; +import java.awt.Font; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; + +import org.apache.commons.io.IOUtils; + +import org.apache.xmlgraphics.java2d.color.CIELabColorSpace; +import org.apache.xmlgraphics.java2d.color.ColorSpaces; +import org.apache.xmlgraphics.java2d.color.DeviceCMYKColorSpace; +import org.apache.xmlgraphics.java2d.color.NamedColorSpace; +import org.apache.xmlgraphics.java2d.ps.EPSDocumentGraphics2D; + +/** + * This example demonstrates how colors are handled when generating PostScript/EPS. + */ +public class EPSColorsExample { + + /** + * Creates an EPS file. The contents are painted using a Graphics2D implementation that + * generates an EPS file. + * @param outputFile the target file + * @throws IOException In case of an I/O error + */ + public static void generateEPSusingJava2D(File outputFile) throws IOException { + OutputStream out = new java.io.FileOutputStream(outputFile); + out = new java.io.BufferedOutputStream(out); + try { + //Instantiate the EPSDocumentGraphics2D instance + EPSDocumentGraphics2D g2d = new EPSDocumentGraphics2D(false); + g2d.setGraphicContext(new org.apache.xmlgraphics.java2d.GraphicContext()); + + //Set up the document size + g2d.setupDocument(out, 400, 200); //400pt x 200pt + + //Paint a bounding box + g2d.drawRect(0, 0, 400, 200); + + g2d.setFont(new Font("sans-serif", Font.BOLD, 14)); + g2d.drawString("Color usage example:", 10, 20); + g2d.setFont(new Font("sans-serif", Font.PLAIN, 12)); + g2d.drawString("RGB", 10, 84); + g2d.drawString("CMYK", 60, 84); + g2d.drawString("(Lab)", 110, 84); + g2d.drawString("(Named)", 160, 84); + + //We're creating a few boxes all filled with some variant of the + //"Postgelb" (postal yellow) color as used by Swiss Post. + + Color colRGB = new Color(255, 204, 0); + g2d.setColor(colRGB); + g2d.fillRect(10, 30, 40, 40); + + //Just convert RGB to CMYK and use that + float[] compsRGB = colRGB.getColorComponents(null); + DeviceCMYKColorSpace cmykCS = ColorSpaces.getDeviceCMYKColorSpace(); + float[] compsCMYK = cmykCS.fromRGB(compsRGB); + Color colCMYK = DeviceCMYKColorSpace.createCMYKColor(compsCMYK); + g2d.setColor(colCMYK); + g2d.fillRect(60, 30, 40, 40); + + //Try CIELab (not implemented, yet) + CIELabColorSpace d50 = ColorSpaces.getCIELabColorSpaceD50(); + Color colLab = d50.toColor(83.25f, 16.45f, 96.89f, 1.0f); + g2d.setColor(colLab); + g2d.fillRect(110, 30, 40, 40); + + //Try named color (Separation, not implemented, yet) + float[] c1xyz = d50.toCIEXYZNative(83.25f, 16.45f, 96.89f); + NamedColorSpace postgelb = new NamedColorSpace("Postgelb", c1xyz); + Color colNamed = new Color(postgelb, new float[] {1.0f}, 1.0f); + g2d.setColor(colNamed); + g2d.fillRect(160, 30, 40, 40); + + //Cleanup + g2d.finish(); + } finally { + IOUtils.closeQuietly(out); + } + } + + /** + * Command-line interface + * @param args command-line arguments + */ + public static void main(String[] args) { + try { + File targetDir; + if (args.length >= 1) { + targetDir = new File(args[0]); + } else { + targetDir = new File("."); + } + if (!targetDir.exists()) { + System.err.println("Target Directory does not exist: " + targetDir); + } + generateEPSusingJava2D(new File(targetDir, "eps-example-colors.eps")); + } catch (Exception e) { + e.printStackTrace(); + } + } + +} diff --git a/examples/java/java2d/ps/EPSExample1.java b/examples/java/java2d/ps/EPSExample1.java new file mode 100644 index 0000000..0650999 --- /dev/null +++ b/examples/java/java2d/ps/EPSExample1.java @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: EPSExample1.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package java2d.ps; + +import java.awt.Color; +import java.awt.Font; +import java.awt.Graphics2D; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; + +import org.apache.commons.io.IOUtils; +import org.apache.xmlgraphics.java2d.ps.EPSDocumentGraphics2D; + +/** + * This example demonstrates how you can generate an EPS (Encapsulated PostScript) file in Java + * using the EPSDocumentGraphics2D class from Apache XML Graphics Commons. + */ +public class EPSExample1 { + + /** + * Creates an EPS file. The contents are painted using a Graphics2D implementation that + * generates an EPS file. + * @param outputFile the target file + * @throws IOException In case of an I/O error + */ + public static void generateEPSusingJava2D(File outputFile) throws IOException { + OutputStream out = new java.io.FileOutputStream(outputFile); + out = new java.io.BufferedOutputStream(out); + try { + //Instantiate the EPSDocumentGraphics2D instance + EPSDocumentGraphics2D g2d = new EPSDocumentGraphics2D(false); + g2d.setGraphicContext(new org.apache.xmlgraphics.java2d.GraphicContext()); + + //Set up the document size + g2d.setupDocument(out, 400, 200); //400pt x 200pt + + //Paint a bounding box + g2d.drawRect(0, 0, 400, 200); + + //A few rectangles rotated and with different color + Graphics2D copy = (Graphics2D)g2d.create(); + int c = 12; + for (int i = 0; i < c; i++) { + float f = ((i + 1) / (float)c); + Color col = new Color(0.0f, 1 - f, 0.0f); + copy.setColor(col); + copy.fillRect(70, 90, 50, 50); + copy.rotate(-2 * Math.PI / (double)c, 70, 90); + } + copy.dispose(); + + //Some text + g2d.rotate(-0.25); + g2d.setColor(Color.RED); + g2d.setFont(new Font("sans-serif", Font.PLAIN, 36)); + g2d.drawString("Hello world!", 140, 140); + g2d.setColor(Color.RED.darker()); + g2d.setFont(new Font("serif", Font.PLAIN, 36)); + g2d.drawString("Hello world!", 140, 180); + + //Cleanup + g2d.finish(); + } finally { + IOUtils.closeQuietly(out); + } + } + + /** + * Command-line interface + * @param args command-line arguments + */ + public static void main(String[] args) { + try { + File targetDir; + if (args.length >= 1) { + targetDir = new File(args[0]); + } else { + targetDir = new File("."); + } + if (!targetDir.exists()) { + System.err.println("Target Directory does not exist: " + targetDir); + } + generateEPSusingJava2D(new File(targetDir, "eps-example1.eps")); + } catch (Exception e) { + e.printStackTrace(); + } + } + +} diff --git a/examples/java/java2d/ps/TilingPatternExample.java b/examples/java/java2d/ps/TilingPatternExample.java new file mode 100644 index 0000000..8040aa6 --- /dev/null +++ b/examples/java/java2d/ps/TilingPatternExample.java @@ -0,0 +1,161 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: TilingPatternExample.java 752824 2009-03-12 10:05:32Z jeremias $ */ + +package java2d.ps; + +import java.awt.Color; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Polygon; +import java.awt.Rectangle; +import java.awt.TexturePaint; +import java.awt.font.GlyphVector; +import java.awt.geom.AffineTransform; +import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; + +import org.apache.commons.io.IOUtils; + +import org.apache.xmlgraphics.image.writer.ImageWriterUtil; +import org.apache.xmlgraphics.java2d.ps.PSDocumentGraphics2D; + +/** + * This example demonstrates the usage of PostScript tiling patterns. The class also generated + * a PNG file so the output can be compared. + */ +public class TilingPatternExample { + + private BufferedImage tile; + private TexturePaint paint; + + /** + * Default constructor. + */ + public TilingPatternExample() { + //Created TexturePaint instance + this.tile = new BufferedImage(40, 20, BufferedImage.TYPE_INT_RGB); + Graphics2D tileg2d = tile.createGraphics(); + tileg2d.setBackground(Color.WHITE); + tileg2d.clearRect(0, 0, tile.getWidth(), tile.getHeight()); + tileg2d.setColor(Color.BLUE); + tileg2d.fillOval(2, 2, tile.getWidth() - 2, tile.getHeight() - 2); + tileg2d.dispose(); + Rectangle2D rect = new Rectangle2D.Double( + 2, 2, + tile.getWidth() / 2.0, tile.getHeight() / 2.0); + this.paint = new TexturePaint(tile, rect); + } + + /** + * Creates a PostScript file. The contents are painted using a Graphics2D implementation. + * @param outputFile the target file + * @throws IOException In case of an I/O error + */ + public void generatePSusingJava2D(File outputFile) throws IOException { + OutputStream out = new java.io.FileOutputStream(outputFile); + out = new java.io.BufferedOutputStream(out); + try { + //Instantiate the PSDocumentGraphics2D instance + PSDocumentGraphics2D g2d = new PSDocumentGraphics2D(false); + g2d.setGraphicContext(new org.apache.xmlgraphics.java2d.GraphicContext()); + + //Set up the document size + g2d.setupDocument(out, 400, 200); //400pt x 200pt + + paintTileAlone(g2d); + paintShapes(g2d); + paintText(g2d); + + g2d.nextPage(); + + paintText(g2d); + + //Cleanup + g2d.finish(); + } finally { + IOUtils.closeQuietly(out); + } + } + + private void generatePNGusingJava2D(File outputFile) throws IOException { + BufferedImage image = new BufferedImage(400, 200, BufferedImage.TYPE_INT_ARGB); + Graphics2D g2d = image.createGraphics(); + paintTileAlone(g2d); + paintShapes(g2d); + paintText(g2d); + g2d.dispose(); + + ImageWriterUtil.saveAsPNG(image, outputFile); + } + + private void paintTileAlone(Graphics2D g2d) { + AffineTransform at = new AffineTransform(); + at.translate(5, 5); + g2d.drawRenderedImage(this.tile, at); + } + + private void paintShapes(Graphics2D g2d) { + g2d.setPaint(paint); + Rectangle rect = new Rectangle(10, 50, 30, 30); + g2d.fill(rect); + rect = new Rectangle(10, 90, 40, 20); + g2d.fill(rect); + Polygon poly = new Polygon(new int[] {50, 100, 150}, new int[] {100, 20, 100}, 3); + g2d.fill(poly); + } + + private void paintText(Graphics2D g2d) { + g2d.setPaint(paint); + Font font = new Font("serif", Font.BOLD, 80); + GlyphVector gv = font.createGlyphVector(g2d.getFontRenderContext(), "Java"); + g2d.translate(100, 180); + g2d.fill(gv.getOutline()); + } + + /** + * Command-line interface + * @param args command-line arguments + */ + public static void main(String[] args) { + try { + File targetDir; + if (args.length >= 1) { + targetDir = new File(args[0]); + } else { + targetDir = new File("."); + } + if (!targetDir.exists()) { + System.err.println("Target Directory does not exist: " + targetDir); + } + File outputFile = new File(targetDir, "tiling-example.ps"); + File pngFile = new File(targetDir, "tiling-example.png"); + TilingPatternExample app = new TilingPatternExample(); + app.generatePSusingJava2D(outputFile); + System.out.println("File written: " + outputFile.getCanonicalPath()); + app.generatePNGusingJava2D(pngFile); + System.out.println("File written: " + pngFile.getCanonicalPath()); + } catch (Exception e) { + e.printStackTrace(); + } + } + +} diff --git a/examples/java/ps/DSCProcessingExample1.java b/examples/java/ps/DSCProcessingExample1.java new file mode 100644 index 0000000..c57b204 --- /dev/null +++ b/examples/java/ps/DSCProcessingExample1.java @@ -0,0 +1,101 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DSCProcessingExample1.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package ps; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.io.InputStream; + +import org.apache.commons.io.IOUtils; +import org.apache.xmlgraphics.ps.dsc.DSCException; +import org.apache.xmlgraphics.ps.dsc.tools.PageExtractor; + +/** + * Demonstrates how the DSC parser can be used to extract a series of pages from a DSC-compliant + * PostScript file. For details how this works, please look into the PageExtractor class + * where the actual functionality is located. This sample class only calls the code there. + */ +public class DSCProcessingExample1 { + + /** + * Extracts a series of pages from a DSC-compliant PostScript file. + * @param srcFile the source PostScript file + * @param tgtFile the target file to write the extracted pages to + * @param from the starting page number + * @param to the ending page number + * @throws IOException In case of an I/O error + */ + public void extractPages(File srcFile, File tgtFile, int from, int to) throws IOException { + InputStream in = new java.io.FileInputStream(srcFile); + in = new java.io.BufferedInputStream(in); + try { + OutputStream out = new java.io.FileOutputStream(tgtFile); + out = new java.io.BufferedOutputStream(out); + try { + PageExtractor.extractPages(in, out, from, to); + } catch (DSCException e) { + throw new RuntimeException(e.getMessage()); + } finally { + IOUtils.closeQuietly(out); + } + } finally { + IOUtils.closeQuietly(in); + } + } + + private static void showInfo() { + System.out.println( + "Call: DSCProcessingExample1 "); + } + + /** + * Command-line interface + * @param args command-line arguments + */ + public static void main(String[] args) { + try { + File srcFile , tgtFile; + int from, to; + if (args.length >= 4) { + srcFile = new File(args[0]); + tgtFile = new File(args[1]); + from = Integer.parseInt(args[2]); + to = Integer.parseInt(args[3]); + } else { + throw new IllegalArgumentException("Invalid number of parameters!"); + } + if (!srcFile.exists()) { + throw new IllegalArgumentException("Source file not found: " + srcFile); + } + DSCProcessingExample1 app = new DSCProcessingExample1(); + app.extractPages(srcFile, tgtFile, from, to); + System.out.println("File written: " + tgtFile.getCanonicalPath()); + } catch (IllegalArgumentException iae) { + System.err.println(iae.getMessage()); + showInfo(); + System.exit(1); + } catch (Exception e) { + e.printStackTrace(); + System.exit(-1); + } + } + +} diff --git a/examples/java/xmp/ExtractMetadataPacket.java b/examples/java/xmp/ExtractMetadataPacket.java new file mode 100644 index 0000000..4971006 --- /dev/null +++ b/examples/java/xmp/ExtractMetadataPacket.java @@ -0,0 +1,98 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ExtractMetadataPacket.java 606567 2007-12-23 15:52:49Z jeremias $ */ + +package xmp; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; + +import javax.xml.transform.TransformerException; + +import org.apache.xmlgraphics.xmp.Metadata; +import org.apache.xmlgraphics.xmp.XMPArray; +import org.apache.xmlgraphics.xmp.XMPConstants; +import org.apache.xmlgraphics.xmp.XMPPacketParser; +import org.apache.xmlgraphics.xmp.XMPProperty; + +/** + * This example shows how to parse an XMP packet from an arbitrary file. + */ +public class ExtractMetadataPacket { + + private static void parseMetadata() throws IOException, TransformerException { + URL url = ExtractMetadataPacket.class.getResource("xmp-sandbox.fop.trunk.pdf"); + InputStream in = url.openStream(); + try { + Metadata meta = XMPPacketParser.parse(in); + if (meta == null) { + System.err.println("No XMP packet found!"); + } else { + dumpSomeMetadata(meta); + } + } finally { + in.close(); + } + } + + private static void dumpSomeMetadata(Metadata meta) { + XMPProperty prop; + prop = meta.getProperty(XMPConstants.DUBLIN_CORE_NAMESPACE, "creator"); + if (prop != null) { + XMPArray array; + array = prop.getArrayValue(); + for (int i = 0, c = array.getSize(); i < c; i++) { + System.out.println("Creator: " + array.getValue(i)); + } + } + prop = meta.getProperty(XMPConstants.DUBLIN_CORE_NAMESPACE, "title"); + if (prop != null) { + System.out.println("Title: " + prop.getValue()); + } + prop = meta.getProperty(XMPConstants.XMP_BASIC_NAMESPACE, "CreateDate"); + if (prop != null) { + System.out.println("Creation Date: " + prop.getValue()); + } + prop = meta.getProperty(XMPConstants.XMP_BASIC_NAMESPACE, "CreatorTool"); + if (prop != null) { + System.out.println("Creator Tool: " + prop.getValue()); + } + prop = meta.getProperty(XMPConstants.ADOBE_PDF_NAMESPACE, "Producer"); + if (prop != null) { + System.out.println("Producer: " + prop.getValue()); + } + prop = meta.getProperty(XMPConstants.ADOBE_PDF_NAMESPACE, "PDFVersion"); + if (prop != null) { + System.out.println("PDF version: " + prop.getValue()); + } + } + + /** + * Command-line interface. + * @param args the command-line arguments + */ + public static void main(String[] args) { + try { + parseMetadata(); + } catch (Exception e) { + e.printStackTrace(); + } + } + +} diff --git a/examples/java/xmp/MergeMetadata.java b/examples/java/xmp/MergeMetadata.java new file mode 100644 index 0000000..f512b85 --- /dev/null +++ b/examples/java/xmp/MergeMetadata.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: MergeMetadata.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package xmp; + +import java.net.URL; +import java.util.Date; + +import javax.xml.transform.TransformerException; + +import org.apache.xmlgraphics.xmp.Metadata; +import org.apache.xmlgraphics.xmp.XMPConstants; +import org.apache.xmlgraphics.xmp.XMPParser; +import org.apache.xmlgraphics.xmp.XMPProperty; +import org.apache.xmlgraphics.xmp.XMPSerializer; +import org.apache.xmlgraphics.xmp.schemas.DublinCoreAdapter; +import org.xml.sax.SAXException; + +/** + * This example shows how to parse an XMP metadata file. + */ +public class MergeMetadata { + + private static void mergeMetadata() throws TransformerException, SAXException { + URL url = MergeMetadata.class.getResource("pdf-example.xmp"); + Metadata meta1 = XMPParser.parseXMP(url); + + Metadata meta2 = new Metadata(); + DublinCoreAdapter dc = new DublinCoreAdapter(meta2); + dc.setTitle("de", "Der Herr der Ringe"); + dc.setTitle("en", "Lord of the Rings"); + dc.addCreator("J.R.R. Tolkien"); //Will replace creator from pdf-example.xmp + dc.addDate(new Date()); + + meta2.mergeInto(meta1); + + Metadata meta = meta1; + XMPProperty prop; + dc = new DublinCoreAdapter(meta); + String[] creators = dc.getCreators(); + for (int i = 0, c = creators.length; i < c; i++) { + System.out.println("Creator: " + creators[i]); + } + System.out.println("Title: " + dc.getTitle()); + System.out.println("Title de: " + dc.getTitle("de")); + System.out.println("Title en: " + dc.getTitle("en")); + prop = meta.getProperty(XMPConstants.XMP_BASIC_NAMESPACE, "CreateDate"); + System.out.println("Creation Date: " + prop.getValue()); + prop = meta.getProperty(XMPConstants.XMP_BASIC_NAMESPACE, "CreatorTool"); + System.out.println("Creator Tool: " + prop.getValue()); + prop = meta.getProperty(XMPConstants.ADOBE_PDF_NAMESPACE, "Producer"); + System.out.println("Producer: " + prop.getValue()); + prop = meta.getProperty(XMPConstants.ADOBE_PDF_NAMESPACE, "PDFVersion"); + System.out.println("PDF version: " + prop.getValue()); + + XMPSerializer.writeXMPPacket(meta, System.out, false); + } + + /** + * Command-line interface. + * @param args the command-line arguments + */ + public static void main(String[] args) { + try { + mergeMetadata(); + } catch (Exception e) { + e.printStackTrace(); + } + } + +} diff --git a/examples/java/xmp/MetadataFromScratch.java b/examples/java/xmp/MetadataFromScratch.java new file mode 100644 index 0000000..2a43d2a --- /dev/null +++ b/examples/java/xmp/MetadataFromScratch.java @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: MetadataFromScratch.java 889803 2009-12-11 20:36:36Z jeremias $ */ + +package xmp; + +import java.util.Date; + +import javax.xml.transform.TransformerConfigurationException; +import javax.xml.transform.stream.StreamResult; + +import org.xml.sax.SAXException; + +import org.apache.xmlgraphics.xmp.Metadata; +import org.apache.xmlgraphics.xmp.XMPSerializer; +import org.apache.xmlgraphics.xmp.schemas.DublinCoreAdapter; + +/** + * This example shows how to build an XMP metadata file from scratch in Java. + */ +public class MetadataFromScratch { + + private static void buildAndPrintMetadata() + throws TransformerConfigurationException, SAXException { + Metadata meta = new Metadata(); + DublinCoreAdapter dc = new DublinCoreAdapter(meta); + dc.setTitle("de", "Der Herr der Ringe"); + dc.setTitle("en", "Lord of the Rings"); + dc.addDate(new Date()); + dc.setFormat("application/pdf"); + dc.addCreator("J.R.R. Tolkien"); + + StreamResult res = new StreamResult(System.out); + XMPSerializer.writeXML(meta, res); + + } + + /** + * Command-line interface. + * @param args the command-line arguments + */ + public static void main(String[] args) { + try { + buildAndPrintMetadata(); + } catch (Exception e) { + e.printStackTrace(); + } + } + +} diff --git a/examples/java/xmp/ParseMetadata.java b/examples/java/xmp/ParseMetadata.java new file mode 100644 index 0000000..8c01eaa --- /dev/null +++ b/examples/java/xmp/ParseMetadata.java @@ -0,0 +1,86 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ParseMetadata.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package xmp; + +import java.net.URL; + +import javax.xml.transform.TransformerException; +import javax.xml.transform.stream.StreamResult; + +import org.xml.sax.SAXException; + +import org.apache.xmlgraphics.xmp.Metadata; +import org.apache.xmlgraphics.xmp.XMPArray; +import org.apache.xmlgraphics.xmp.XMPConstants; +import org.apache.xmlgraphics.xmp.XMPParser; +import org.apache.xmlgraphics.xmp.XMPProperty; +import org.apache.xmlgraphics.xmp.XMPSerializer; +import org.apache.xmlgraphics.xmp.schemas.DublinCoreAdapter; +import org.apache.xmlgraphics.xmp.schemas.DublinCoreSchema; + +/** + * This example shows how to parse an XMP metadata file. + */ +public class ParseMetadata { + + private static void parseMetadata() throws TransformerException, SAXException { + URL url = ParseMetadata.class.getResource("pdf-example.xmp"); + Metadata meta = XMPParser.parseXMP(url); + XMPProperty prop; + prop = meta.getProperty(XMPConstants.DUBLIN_CORE_NAMESPACE, "creator"); + XMPArray array; + array = prop.getArrayValue(); + for (int i = 0, c = array.getSize(); i < c; i++) { + System.out.println("Creator: " + array.getValue(i)); + } + prop = meta.getProperty(XMPConstants.DUBLIN_CORE_NAMESPACE, "title"); + array = prop.getArrayValue(); + System.out.println("Default Title: " + array.getSimpleValue()); + System.out.println("German Title: " + array.getLangValue("de")); + prop = meta.getProperty(XMPConstants.XMP_BASIC_NAMESPACE, "CreateDate"); + System.out.println("Creation Date: " + prop.getValue()); + prop = meta.getProperty(XMPConstants.XMP_BASIC_NAMESPACE, "CreatorTool"); + System.out.println("Creator Tool: " + prop.getValue()); + prop = meta.getProperty(XMPConstants.ADOBE_PDF_NAMESPACE, "Producer"); + System.out.println("Producer: " + prop.getValue()); + prop = meta.getProperty(XMPConstants.ADOBE_PDF_NAMESPACE, "PDFVersion"); + System.out.println("PDF version: " + prop.getValue()); + + DublinCoreAdapter dc = DublinCoreSchema.getAdapter(meta); + System.out.println("Default title: " + dc.getTitle()); + System.out.println("German title: " + dc.getTitle("de")); + + StreamResult res = new StreamResult(System.out); + XMPSerializer.writeXML(meta, res); + } + + /** + * Command-line interface. + * @param args the command-line arguments + */ + public static void main(String[] args) { + try { + parseMetadata(); + } catch (Exception e) { + e.printStackTrace(); + } + } + +} diff --git a/examples/java/xmp/pdf-example.xmp b/examples/java/xmp/pdf-example.xmp new file mode 100644 index 0000000..06f45ad --- /dev/null +++ b/examples/java/xmp/pdf-example.xmp @@ -0,0 +1,28 @@ + + + + + + + John Doe + Jane Doe + + + + + Example document + Beispieldokument + + + 2006-06-02T10:36:40+02:00 + + + 2006-06-02T10:36:40+02:00 + An XML editor + + + Apache FOP Version SVN trunk + 1.4 + + + diff --git a/examples/java/xmp/xmp-sandbox.fop.trunk.pdf b/examples/java/xmp/xmp-sandbox.fop.trunk.pdf new file mode 100644 index 0000000..5d75cd4 Binary files /dev/null and b/examples/java/xmp/xmp-sandbox.fop.trunk.pdf differ diff --git a/lib/README.txt b/lib/README.txt new file mode 100644 index 0000000..22ff3e4 --- /dev/null +++ b/lib/README.txt @@ -0,0 +1,55 @@ + +Information on Apache XML Graphics Commons dependencies +========================================================== + +$Id: README.txt 606580 2007-12-23 17:45:02Z jeremias $ + +The Apache Licenses can also be found here: +http://www.apache.org/licenses/ + + +Normal Dependencies +---------------------- + +- Apache Commons IO + + commons-io-*.jar + http://jakarta.apache.org/commons/io/ + (I/O routines) + + Apache License v2.0 + + +- Apache Commons Logging + + commons-logging-*.jar + http://jakarta.apache.org/commons/logging/ + (Logger abstraction) + + Apache License v2.0 + + + +Special Dependencies +----------------------- + +- JAXP 1.2 (Java 1.4 level) + + + +Additional development-time dependencies +------------------------------------------- + +- Apache Ant (1.6.5 or later) + + (not bundled, requires pre-installation) + http://ant.apache.org + (XML-based build system + + Apache License V2.0 + +- JUnit (3.8.1 or later) + + (not bundled, provided by Apache Ant or your IDE) + http://www.junit.org + Common Public License V1.0 \ No newline at end of file diff --git a/lib/build/hamcrest.core-1.1.0.LICENSE.txt b/lib/build/hamcrest.core-1.1.0.LICENSE.txt new file mode 100644 index 0000000..e3d4feb --- /dev/null +++ b/lib/build/hamcrest.core-1.1.0.LICENSE.txt @@ -0,0 +1,27 @@ +BSD License + +Copyright (c) 2000-2006, www.hamcrest.org +All 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. + +Neither the name of Hamcrest nor the names of its contributors may 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. \ No newline at end of file diff --git a/lib/build/hamcrest.core-1.1.0.jar b/lib/build/hamcrest.core-1.1.0.jar new file mode 100644 index 0000000..5b2b484 Binary files /dev/null and b/lib/build/hamcrest.core-1.1.0.jar differ diff --git a/lib/build/mockito-core-2.28.2.LICENCE.txt b/lib/build/mockito-core-2.28.2.LICENCE.txt new file mode 100644 index 0000000..e0840a4 --- /dev/null +++ b/lib/build/mockito-core-2.28.2.LICENCE.txt @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2007 Mockito contributors + +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. \ No newline at end of file diff --git a/lib/build/mockito-core-2.28.2.NOTICE.txt b/lib/build/mockito-core-2.28.2.NOTICE.txt new file mode 100644 index 0000000..0594174 --- /dev/null +++ b/lib/build/mockito-core-2.28.2.NOTICE.txt @@ -0,0 +1,11 @@ +Mockito license - MIT. + +Libraries used: + +Cglib - Apache License 2.0 +ASM - BSD license + +Mockito all distribution: + +Objenesis - MIT license +Hamcrest - BSD license \ No newline at end of file diff --git a/lib/build/mockito-core-2.28.2.jar b/lib/build/mockito-core-2.28.2.jar new file mode 100644 index 0000000..525ff47 Binary files /dev/null and b/lib/build/mockito-core-2.28.2.jar differ diff --git a/lib/build/objenesis-1.0.0.LICENSE.txt b/lib/build/objenesis-1.0.0.LICENSE.txt new file mode 100644 index 0000000..74582ac --- /dev/null +++ b/lib/build/objenesis-1.0.0.LICENSE.txt @@ -0,0 +1,18 @@ +Copyright (c) 2003-2008, Objenesis Team and all contributors + +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. \ No newline at end of file diff --git a/lib/build/objenesis-1.0.0.jar b/lib/build/objenesis-1.0.0.jar new file mode 100644 index 0000000..1f1b76d Binary files /dev/null and b/lib/build/objenesis-1.0.0.jar differ diff --git a/lib/build/resolver-1.2.LICENSE.txt b/lib/build/resolver-1.2.LICENSE.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/lib/build/resolver-1.2.LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/lib/build/resolver-1.2.NOTICE.txt b/lib/build/resolver-1.2.NOTICE.txt new file mode 100644 index 0000000..f5876d2 --- /dev/null +++ b/lib/build/resolver-1.2.NOTICE.txt @@ -0,0 +1,16 @@ + ========================================================================= + == NOTICE file corresponding to section 4(d) of the Apache License, == + == Version 2.0, in this case for the Apache xml-commons xml-apis == + == distribution. == + ========================================================================= + + Apache XML Commons + Copyright 2001-2003,2006 The Apache Software Foundation. + + This product includes software developed at + The Apache Software Foundation (http://www.apache.org/). + + Portions of this software were originally based on the following: + - software copyright (c) 1999, IBM Corporation., http://www.ibm.com. + - software copyright (c) 1999, Sun Microsystems., http://www.sun.com. + - software copyright (c) 2000 World Wide Web Consortium, http://www.w3.org diff --git a/lib/build/resolver-1.2.jar b/lib/build/resolver-1.2.jar new file mode 100644 index 0000000..e535bdc Binary files /dev/null and b/lib/build/resolver-1.2.jar differ diff --git a/lib/commons-io-1.3.1.jar b/lib/commons-io-1.3.1.jar new file mode 100644 index 0000000..7affdef Binary files /dev/null and b/lib/commons-io-1.3.1.jar differ diff --git a/lib/commons-io.LICENSE.txt b/lib/commons-io.LICENSE.txt new file mode 100644 index 0000000..6b0b127 --- /dev/null +++ b/lib/commons-io.LICENSE.txt @@ -0,0 +1,203 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/lib/commons-io.NOTICE.txt b/lib/commons-io.NOTICE.txt new file mode 100644 index 0000000..ce3b94a --- /dev/null +++ b/lib/commons-io.NOTICE.txt @@ -0,0 +1,6 @@ +Apache Jakarta Commons IO +Copyright 2001-2007 The Apache Software Foundation + +This product includes software developed by +The Apache Software Foundation (http://www.apache.org/). + diff --git a/lib/commons-logging-1.0.4.jar b/lib/commons-logging-1.0.4.jar new file mode 100644 index 0000000..b73a80f Binary files /dev/null and b/lib/commons-logging-1.0.4.jar differ diff --git a/lib/commons-logging.LICENSE.txt b/lib/commons-logging.LICENSE.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/lib/commons-logging.LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/lib/commons-logging.NOTICE.txt b/lib/commons-logging.NOTICE.txt new file mode 100644 index 0000000..439eb83 --- /dev/null +++ b/lib/commons-logging.NOTICE.txt @@ -0,0 +1,3 @@ +This product includes software developed by +The Apache Software Foundation (http://www.apache.org/). + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..eb67c1a --- /dev/null +++ b/pom.xml @@ -0,0 +1,234 @@ + + 4.0.0 + + org.apache.xmlgraphics + xmlgraphics-commons + 2.7 + ${project.groupId}:${project.artifactId} + XML Graphics Commons + http://xmlgraphics.apache.org/commons/ + + + 2.15 + 1.4.0 + 3.0.4 + 1.7 + 4.11 + 2.8 + UTF-8 + 2.5.2 + 2.18.1 + ${env.JAVA_HOME} + + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + scm:svn:https://svn.apache.org/repos/asf/xmlgraphics/commons/trunk/ + scm:svn:https://svn.apache.org/repos/asf/xmlgraphics/commons/trunk/ + + + + + Apache Software Foundation + http://www.apache.org/ + + + + + ${project.artifactId}-site + ${project.baseUri} + + + + + + commons-io + commons-io + 1.3.2 + + + commons-logging + commons-logging + 1.2 + + + junit + junit + ${junit.version} + test + + + org.mockito + mockito-core + 2.28.2 + test + + + xml-resolver + xml-resolver + 1.2 + test + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${java.version} + ${java.version} + + + + org.apache.maven.plugins + maven-project-info-reports-plugin + ${project.info.reports.version} + + + org.apache.maven.plugins + maven-surefire-plugin + ${surefire.version} + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + ${checkstyle.version} + + ${project.baseUri}src/tools/resources/checkstyle/checkstyle.xml + ${project.baseUri}src/tools/resources/checkstyle/LICENSE.txt + false + false + false + true + ${project.baseUri}src/tools/resources/checkstyle/suppressions.xml + warning + + + + org.codehaus.mojo + findbugs-maven-plugin + ${findbugs.version} + + src/tools/resources/findbugs/exclusions.xml + Max + Low + + + + maven-surefire-plugin + ${surefire.version} + + + **/*TestCase.java + **/*TestCases.java + + true + + + + + + src/main/resources + + + ${basedir} + + LICENSE + NOTICE + + META-INF + + + + + + src/test/java + + **/*.png + **/*.tiff + **/*.txt + **/*.xmp + + + + src/test/resources + + **/* + + + + + + + + custom-javac + + + jdk.path + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${java.version} + ${java.version} + ${jdk.path}/bin/javac + true + + + + + + + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + ${checkstyle.version} + + ${project.baseUri}src/tools/resources/checkstyle/checkstyle.xml + ${project.baseUri}src/tools/resources/checkstyle/LICENSE.txt + false + false + false + ${project.baseUri}src/tools/resources/checkstyle/suppressions.xml + + + + org.codehaus.mojo + findbugs-maven-plugin + ${findbugs.version} + + src/tools/resources/findbugs/exclusions.xml + Max + true + true + Low + true + + + + + + diff --git a/src/documentation/README.txt b/src/documentation/README.txt new file mode 100644 index 0000000..9bc261b --- /dev/null +++ b/src/documentation/README.txt @@ -0,0 +1,7 @@ +This is the base documentation directory. + +skinconf.xml # This file customizes Forrest for your project. In it, you + # tell forrest the project name, logo, copyright info, etc + +sitemap.xmap # Optional. This sitemap is consulted before all core sitemaps. + # See http://forrest.apache.org/docs/project-sitemap.html diff --git a/src/documentation/classes/CatalogManager.properties b/src/documentation/classes/CatalogManager.properties new file mode 100644 index 0000000..7ca47f0 --- /dev/null +++ b/src/documentation/classes/CatalogManager.properties @@ -0,0 +1,38 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#======================================================================= +# CatalogManager.properties +# +# This is the default properties file for Apache Forrest. +# This facilitates local configuration of application-specific catalogs. +# +# See the Apache Forrest documentation: +# http://forrest.apache.org/docs/your-project.html +# http://forrest.apache.org/docs/validation.html + +# verbosity ... level of messages for status/debug +# See forrest/src/core/context/WEB-INF/cocoon.xconf + +# catalogs ... list of additional catalogs to load +# (Note that Apache Forrest will automatically load its own default catalog +# from src/core/context/resources/schema/catalog.xcat) +# use full pathnames +# pathname separator is always semi-colon (;) regardless of operating system +# directory separator is always slash (/) regardless of operating system +# +#catalogs=/home/me/forrest/my-site/src/documentation/resources/schema/catalog.xcat +catalogs= + diff --git a/src/documentation/content/doap.rdf b/src/documentation/content/doap.rdf new file mode 100644 index 0000000..99025dd --- /dev/null +++ b/src/documentation/content/doap.rdf @@ -0,0 +1,101 @@ + + + + + 2006-04-17 + + Apache XML Graphics Commons + + + Common components for Apache Batik and Apache FOP + Apache XML Graphics Commons is a library that consists of several reusable components used by Apache Batik and Apache FOP. Many of these components can easily be used separately outside the domains of SVG and XSL-FO. You will find components such as a PDF library, an RTF library, Graphics2D implementations that let you generate PDF and PostScript files and much more. + + + Java + + + + PostScript + Adobe Systems Incorporated + PS + + + + + + Extensible Metadata Platform (XMP) + Adobe Systems Incorporated + XMP + + + + + + Current release (stable) + 2012-10-20 + 1.5 + + + Previous release (stable) + 2010-07-07 + 1.4 + + + Previous release (stable) + 2008-06-11 + 1.3.1 + + + Previous release (stable) + 2008-03-07 + 1.3 + + + Previous release (stable) + 2007-07-21 + 1.2 + + + Previous release (stable) + 2006-12-22 + 1.1 + + + Previous release (stable) + 2006-04-17 + 1.0 + + + + + + + + + + + Jeremias Maerki + + + + + diff --git a/src/documentation/content/xdocs/bugs.xml b/src/documentation/content/xdocs/bugs.xml new file mode 100644 index 0000000..4b89796 --- /dev/null +++ b/src/documentation/content/xdocs/bugs.xml @@ -0,0 +1,67 @@ + + + + + + +
+ Apache™ XML Graphics Commons: Bugs and Other Trackable Issues + $Revision: 911792 $ +
+ +

+ Information on this page applies to enhancement requests and other trackable +issues as well as bugs. +

+
+ Reported Issues +

A list of unresolved reported bugs can be found at XML Graphics Commons Open Bugs (Bugzilla). If you have an +interest in an issue already reported, please consider the +following: +

+
    +
  • If you have insight that may help developers solve an existing problem, +please add comments and/or file attachments to the existing issue.
  • +
  • If you would like to track the status of the issue, consider adding +your email address to the list of "CC" recipients, so that you will receive +an email as changes are made to the issue.
  • +
+
+
+ Unreported Issues (Reporting New Issues) +

User reports of bugs and requests for enhancements are extremely +important parts of XML Graphics Commons development, and we appreciate the time you take to help +us track these issues down.

+
    + +
  • Review the Apache Bug Writing Guidelines before submitting your report.
  • +
  • Enter a new issue report at the XML Graphics Commons issue database (Bugzilla). +You will be asked to login to an existing Bugzilla account or to create a new +one. +When entering the bug report, please make your description complete and concise. +If appropriate, attach a minimal fo file to your report which demonstrates the +problem.
  • +
  • After submission, a copy of your bug report will be automatically +sent to the XML Graphics General discussion list.
  • +
+
+ +
+ diff --git a/src/documentation/content/xdocs/download.xml b/src/documentation/content/xdocs/download.xml new file mode 100644 index 0000000..b0b9240 --- /dev/null +++ b/src/documentation/content/xdocs/download.xml @@ -0,0 +1,63 @@ + + + + +
+ Download Apache™ XML Graphics Commons +
+ +
+ Download a Release +

+ Source ("-src") and binary ("-bin") distributions can be downloaded from a + Apache™ XML Graphics Commons Distribution Mirror. +

+
+
+ Download from Subversion +

+ The latest source code is available directly from the Subversion repository: +

+ + + + + + + + + + + + +
Trunk
Repository URL + + http://svn.apache.org/repos/asf/xmlgraphics/commons/trunk/ + +
Web view + + http://svn.apache.org/viewvc/xmlgraphics/commons/trunk/ + +
+ + Committers need to replace "http" with "https" + and then log in so they can gain write access! + +
+ +
diff --git a/src/documentation/content/xdocs/favicon.ico b/src/documentation/content/xdocs/favicon.ico new file mode 100644 index 0000000..f0c22ad Binary files /dev/null and b/src/documentation/content/xdocs/favicon.ico differ diff --git a/src/documentation/content/xdocs/image-loader.xml b/src/documentation/content/xdocs/image-loader.xml new file mode 100644 index 0000000..b79d44f --- /dev/null +++ b/src/documentation/content/xdocs/image-loader.xml @@ -0,0 +1,360 @@ + + + + +
+ Apache™ XML Graphics Commons: Image Loader Framework +
+ +
+ Overview +

+ Apache™ XML Graphics Commons contains a unified framework for loading and + processing images (bitmap and vector). The package name is + org.apache.xmlgraphics.image.loader. Key features: +

+
    +
  • + Unified basic API for all supported image types. +
  • +
  • + Image "Preloading": It allows automatic detection on the image + content type and can extract the intrinsic size (in pixels and length + units) of the image without loading the whole image into memory in + most cases. Apache FOP + uses this as it only needs the size of the image to do the layout. + The image is only fully read at the rendering stage. +
  • +
  • + Image conversion facility: Images can be converted into different + representations depending on the needs of the consumer. +
  • +
  • + Supported formats: All bitmap formats for which there are codecs + for the ImageIO API (like JPEG, PNG, GIF etc.), EPS, EMF. These + formats are bundled. Other formats such as SVG and WMF are available + through plug-ins hosted elsewhere. +
  • +
  • + Supported in-memory representations: +
      +
    • RenderedImage/BufferedImage
    • +
    • raw/undecoded (JPEG, EPS, CCITT group 3/4)
    • +
    • Java2D (Images painted through Graphics2D)
    • +
    • XML DOM (for SVG, MathML etc.)
    • +
    • Additional representations can be added as necessary.
    • +
    +
  • +
  • + Custom image loaders and converters can be dynamically plugged in. + Automatic plug-in detection through the application classpath. +
  • +
  • + An image cache speeds up the processing for images that are requested + multiple times. +
  • +
+
+
+ Tutorial +
+ Setting up the manager +

+ Before we can start to work with the package we need to set up the + ImageManager. It provides convenience methods to load + and convert images and holds the image cache. +

+

+ The ImageManager needs an ImageContext. + This interface provides the ImageManager with important + context and configuration data. Currently this is only the source + resolution. The ImageManager and + ImageContext are intended to be shared within an + application. +

+ + + In this example, DefaultImageContext is used. You may + need to write your own implementation of ImageContext for + your use case. + +
+
+ Preloading an image +

+ In order to load an image, it needs to be "preloaded" first, i.e. the image content + type is detected and the intrinsic size of the image is determined. The result of this + process is an ImageInfo instance which contains the URI, MIME type and + intrinsic size. In most cases, this is done without loading the whole image (see + SPI section below for information on exceptions to this rule). +

+

+ Preloading is normally done through the ImageManager's + getImageInfo() method. For this operation + an ImageSessionContext needs to be provided. It is responsible for + supplying JAXP Source objects, URI resolution and providing other + information needed for the image operations. In simple cases you can simply use + DefaultImageSessionContext, but often you will want to write your own + implementation of ImageSessionContext. In that case, it's recommended to + subclass AbstractImageSessionContext which lets you avoid rewriting a + lot of code for providing Source objects. +

+

+

+ +
+
+ Loading an image +

+ Once the image is "preloaded", it can be fully loaded in the form/flavor that is + needed by the consuming application. The required flavor is indicated through the + ImageFlavor class. If you want the image as a bitmap image in memory, + you could request an ImageFlavor.RENDERED_IMAGE. Again, the + ImageSessionContext will be needed. +

+ +

+ In this example above, we simply acquire the image as a RenderedImage instance. + If the original image was a vector graphic image (SVG, WMF etc.), it's automatically + converted to a bitmap image. Note: The resolution of the created image is controlled + by the target resolution returned by the ImageSessionContext. +

+

+ Of course, the framework can only provide images in the formats, it has image loaders + or image converters for. An example: It is possible to load EPS images, but they + can only be provided in raw form. In order to provide it as a bitmap image, a PostScript + interpreter would be needed to interpret the PostScript code. This interpreter would + be integrated using an ImageConverter implementation (see SPI section below). + If the requested form of the image cannot be provided you will get an + ImageException on which you'll have to react as needed. +

+

+ In Apache FOP, each renderer supports + a different set of image flavors that can be embedded in the target format. For example: + The PDF renderer can deal with Java2D image, bitmaps, XML, native JPEG and CCITT images. + The PCL renderer, however, can only consume bitmap images. So, if you can accept + more than one flavor, the package allows you to specify all of them in an ordered list + (the first in the list is the preferred format). The package will then try to return + the best representation possible. Here's a code example: +

+ + + While each BufferedImage is also a RenderedImage, + it can be more efficient to also specify ImageFlavor.BUFFERED_IMAGE in + the flavor array. + +
+
+
+ Tips & Tricks +

+ If you are loading bitmap images and you get an error like + "Cannot load image (no suitable loader/converter combination available) for + myimage.tif (image/tiff), + you maybe be missing the necessary ImageIO codec to decode the image. A number of + well-written codecs can be found in + JAI Image I/O Tools Project. Just download + the distribution and add the JAR to the classpath. ImageIO will automatically pick up + the new codecs and they will subsequently be available to the image framework. +

+
+
+ Service Provider Interface (SPI, Plug-ins) +

+ The whole image framework is designed to be highly extensible. There are various + extension points where new functionality can be added. The three main SPI interfaces are: +

+
    +
  • ImagePreloader: detects the content type and preloads an image
  • +
  • ImageLoader and ImageLoaderFactory: loads images
  • +
  • ImageConverter: converts images from one representation into another
  • +
+

+ If you plan to write an implementation of one of the above interfaces, please also take + a look at the existing implementations for reference. +

+

+ Throughout the SPI, you'll find a Map parameter (hints) in the most important + methods. That's a way to supply additional information to the implementation by the + caller. For example, the source and target resolutions from the image (session) context + is stored in the hints. The implementation should not rely on the presence of specialized + information and should always have sensible defaults to rely on in this case. +

+
+ ImagePreloader +

+ The first task is identifying whether the implementation supports the given image. + If the image is loaded using an ImageInputStream it is important to always reset the + stream position to the beginning of the file at the end of the + preloadImage() method, because all registered preloaders are check in turn + until one implementation signals that it supports the format. In that case, it has to + extract only the minimal information from the image necessary to identify the image's + intrinsic size. For most formats, this is doable without loading the whole image into + memory. +

+

+ However, for some formats (like MathML or WMF), loading the whole image at preloading + time is hard to avoid since the image's size can only be determined that way. In such + a case, the ImagePreloader implementations shall pass the loaded + document to the respective ImageLoader through the custom objects that + can be attached to the ImageInfo object. If the preloader loads the whole + document, it shall close the given Source object (calling + ImageUtil.closeQuietly(Source)). +

+

+ The priority the implementation reports is used to sort all registered implementations. + This is to fine-tune the inner workings and to optimize performance since some formats + are usually used more frequently than others. +

+ + Normally, if you implement an ImagePreloader you will also need to implement + the respective ImageLoader/ImageLoaderFactory, or vice versa. + +
+
+ ImageLoader and ImageLoaderFactory +

+ The factory interface has been created to allow checking if some library that an + implementation depends on is really in the classpath so it can report back that the + ImageLoader is not funtional. The factory also reports what kind of + image formats it supports and which image flavors it can return. There can be a + complex relationship between the two. It is recommmended, however, to write smaller + implementations rather than big, almighty ones. +

+

+ The usage penalty is used when constructing image conversion pipelines. There can be + multiple ways to provide an image in one of the supported flavors and this value helps + to make the best decision. +

+

+ While the factory basically just provides information and creates new + ImageLoader instances, the image loaders are doing the actual leg work + of decoding the images. The image flavor returned by the loader must match the + flavor that is returned by getTargetFlavor(). +

+
+
+ ImageConverter +

+ The image converter is responsible to transform one image representation into another. + Bundled implementations support these conversions: Java2D to bitmap, bitmap to Java2D + and RenderedImage to "raw" PNG. Ideas for additional image converters could be: + PDF to Java2D, EPS to Java2D or MathML to SVG or Java2D. +

+

+ Each ImageConverter comes with a usage penalty which is used when constructing + conversion pipelines so the pipeline with the least penalty value can be chosen. This + is necessary as the consuming application my support multiple image flavors and there + can be multiple ways to convert an image in one of the requested image flavors. + Internally, Dijkstra's + shortest path algorithm is used to find the best path using the penalties as "way + lengths". +

+
+
+
+ Customization +
+ Disabling Source Re-use +

+ By default, the Source object being used during the pre-loading stage is re-used when + the image is fully loaded later (assuming an ImageSessionContext is used that descends + from AbstractImageSessionContext). That means that a stream is only opened once and + the image loading framework tries to re-wind the stream when it has to re-read portions + of the stream when loading the complete image. +

+

+ In some situations, this behavior may be undesired. Therefore, it can be disabled + through a system property + (org.apache.xmlgraphics.image.loader.impl.AbstractImageSessionContext.no-source-reuse). + Set it to "true" and that feature will be disabled. +

+
+
+ Adjusting plug-in penalties +

+ Every image loader plug-in has a hard-coded usage penalty that influences which solution + is chosen if there are multiple possibilities to load an image. Sometimes, though, these + penalties need to be tweaked. The ImageImplRegistry (reachable through + ImageManager.getRegistry()) supports storing additional penalty values. + An example: +

+ +

+ This increases the penalty for the raw CCITT loader significantly so it will only be + used if no other solutions exist. You can also set Penalty.INFINITE_PENALTY + to disable the plug-in altogether. Negative penalties are possible to promote a plug-in + but a negative penalty sum will be treated as zero penalty in most cases. +

+
+
+ +
diff --git a/src/documentation/content/xdocs/index.xml b/src/documentation/content/xdocs/index.xml new file mode 100644 index 0000000..4141f0e --- /dev/null +++ b/src/documentation/content/xdocs/index.xml @@ -0,0 +1,112 @@ + + + + +
+ Apache™ XML Graphics Commons +
+ +
+ Overview +

+ Apache™ XML Graphics Commons is a library that consists of several reusable components + used by Apache Batik and Apache FOP. + Many of these components can easily be used separately outside the domains of SVG + and XSL-FO. You will find components such as a PDF library, an RTF library, Graphics2D + implementations that let you generate PDF & PostScript files, and much more. +

+

The Apache™ XML Graphics Commons project is part of the Apache™ + Software Foundation, which is a wider community of users and developers of open + source projects. +

+

+ In the Wiki, we have a + Roadmap for Apache XML Graphics Commons. + This roadmap is the place to describe new ideas for the project. +

+
+
+ Features +

+ Components which have been ported from Apache Batik and + Apache FOP include: +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DescriptionPackageProvenance
Image codecs for PNG and TIFForg.apache.xmlgraphics.image.codecBatik
Bitmap image writer abstraction with implementation for the above codecs and for the Image I/O API.org.apache.xmlgraphics.image.writerBatik
Java2D helper classesorg.apache.xmlgraphics.image.rendered and org.apache.xmlgraphics.java2dBatik
Image loader framework (format detection, conversion and unified handling for bitmap and vector images).org.apache.xmlgraphics.image.loadernew
Graphics2D implementation that produces PostScript and EPSorg.apache.xmlgraphics.java2d.psFOP
Helper classes for PostScript code productionorg.apache.xmlgraphics.psFOP
Parser/Processor for DSC-compliant PostScript files (DSC = + Document Structuring Conventions) + org.apache.xmlgraphics.ps.dscnew
XMP metadata frameworkorg.apache.xmlgraphics.xmpnew
Various I/O classes, encoders and decoders for various formatsorg.apache.xmlgraphics.util.ioBatik/FOP
+
+
+ News +

+ RSS Feed: Subproject News Feed +

+ +
+ +
diff --git a/src/documentation/content/xdocs/news.xml b/src/documentation/content/xdocs/news.xml new file mode 100644 index 0000000..ab81b88 --- /dev/null +++ b/src/documentation/content/xdocs/news.xml @@ -0,0 +1,66 @@ + + + +

+ This release consists primarily of bug fixes, improved code practices, and improved coverage of + CCITTFax Group 3 image formats. For details, please see + Changes. +

+
+ +

+ This release adds the option to generate smaller PostScript + files, support for the AdobeStandardCyrillic encoding, + RefinedImageFlavor, TexturePaint support for PSGraphics2D + (PostScript tiling patterns), improvements to the XMP framework, + optimization for PostScript state handling in + (E)PSDocumentGraphics2D, and more. In addition it contains a + number of bug fixes. For details, please see the Changes page. +

+

+ We are pleased to note that many new features were contributed + from outside the team of committers. Such broader interest + ensures the healthy further development of the project. +

+
+ +

+ This release is mostly a bugfix release for the image loading framework + that has been introduced in version 1.3. For details, please see the + Changes page. +

+
+ +

+ The most important addition in this release is an image loading framework + which supports all sorts of different image formats (bitmap and vector) and + is highly extensible. +

+

+ Besides that there were a larger number of smaller additions and bugfixes. +

+

+ Support for Java 1.3 has been dropped. Java 1.4 or later is required now. +

+
+ +

+ This release mainly adds support for CMYK and GRAY color spaces for + PSGenerator. For details about other fixes, please see the + Changes page. +

+
+ +

+ This release adds an XMP metadata framework and brings improvements for the + ImageWriter package plus some minor fixes mainly in the PostScript area. +

+
+ +

+ This is the first release of Apache XML Graphics Commons. There are currently no + known issues with the code. +

+
+
diff --git a/src/documentation/content/xdocs/postscript.xml b/src/documentation/content/xdocs/postscript.xml new file mode 100644 index 0000000..35e0920 --- /dev/null +++ b/src/documentation/content/xdocs/postscript.xml @@ -0,0 +1,142 @@ + + + + +
+ Tools for Adobe PostScript +
+ +
+ Overview +

+ Apache™ XML Graphics Commons contains various tools for writing and processing Adobe + PostScript files. This includes: +

+
    +
  • A PostScript generator class which helps writing PostScript files from scratch.
  • +
  • Two Graphics2D implementations, one for plain PostScript and one for writing + Encapsulated PostScript (EPS).
  • +
  • A DSC-parser/processor: Parse, post-process and change DSC-compliant PostScript files.
  • +
+ + We don't currently include a PostScript interpreter though we would love to have one. A + Java-based PostScript interpreter to keep an eye on is the one from the + FOray project. + +
+
+ The PostScript generator +

+ The "PSGenerator" class can help writing PostScript files. It deals with things like + escaping, saving/tracking/restoring graphics state, writing DSC comments and tracking of + DSC resources. +

+

+ You will rarely interact with the PS generator itself, as it is probably more interesting + to generate a PostScript file using Java2D which is described in the following section. +

+
+
+ Java2D: Graphics2D implementation for generating PostScript and EPS +

+ We provide two classes (PSDocumentGraphics2D and EPSDocumentGraphics2D) which you can use + to generated complete PostScript files using normal Java2D means. The difference between + the two classes is that the EPS variant creates a fully compliant Encapsulated + PostScript file while the PS variant simply creates a normal DSC-compliant level 2 + PostScript file. It depends on your requirement which variant you choose. The PS variant + is mostly for printing purposes while the EPS variant is better suited for inclusion in + other documents. +

+
+ Creating an EPS file +

+ Creating an EPS file using the Graphics2D implementation is easy. Instantiate + EPSDocumentGraphics2D, set a GraphicContext and set up the output document. Here's an + example: +

+ +

+ A complete example for generating an EPS files can be found in the + "examples" directory + in the distribution. +

+
+
+
+ DSC parser/processor +

+ Many PostScript files use special comments to structure a document. This allows manipulation + of PostScript files without interpreting them. These special comments are defined in the + Document Structuring Conventions. + The code in Commons is designed to work with DSC 3.0. For details on how DSC is used, + please take a look at the DSC specification. +

+

+ The DSC support in Commons was primarily developed to implement resource optimization + features in Apache FOP's PostScript output support. Resources like + images which are used repeatedly in a document should not be written to the PostScript + file each time it is used. Instead it is written once at the beginning of the file as a + PostScript form. The form is then called whenever the image needs painting. +

+

+ But the DSC parser could potentially be used for other purposes. The most obvious is + extracting a subset of pages from a DSC-compliant file. Assume you want to print only + page 45 to 57 of a particular document. There's an example that demonstrates exactly this. + Check out the "examples" directory in the distribution. Other potential use cases for the + DSC parser are: +

+
    +
  • Patching PostScript files, for example, adding OMR marks for automatic packaging
  • +
  • Imposition (2-up, n-up, rotation, etc.)
  • +
  • EPS graphic extraction
  • +
  • Inspecting the page count
  • +
  • etc. etc.
  • +
+

+ The DSC parser (DSCParser) was designed as a pull parser, i.e. you fetch new events from + the parser inspecting them and acting on them as they are found. If you prefer to work + with a push parser, you can pass the DSCParser a DSCHandler implementation and the parser + will send you all the events. +

+

+ The best example to understand how to use the DSC parser is the PageExtractor class + that implements the page extraction functionality mentioned above. +

+ + The DSC parser is not considered feature-complete. The basic infrastructure is there but, + for example, not all DSC comments are available as concrete Java classes. If you need + to extend the DSC parser for your own use cases, please send us your patches. + +
+ +
diff --git a/src/documentation/content/xdocs/site.xml b/src/documentation/content/xdocs/site.xml new file mode 100644 index 0000000..af8ee20 --- /dev/null +++ b/src/documentation/content/xdocs/site.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + " + " + " + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/documentation/content/xdocs/tabs.xml b/src/documentation/content/xdocs/tabs.xml new file mode 100644 index 0000000..85258da --- /dev/null +++ b/src/documentation/content/xdocs/tabs.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + diff --git a/src/documentation/resources/images/apache-xml-graphics.gif b/src/documentation/resources/images/apache-xml-graphics.gif new file mode 100644 index 0000000..c5d9139 Binary files /dev/null and b/src/documentation/resources/images/apache-xml-graphics.gif differ diff --git a/src/documentation/resources/images/apache-xml-graphics.jpg b/src/documentation/resources/images/apache-xml-graphics.jpg new file mode 100644 index 0000000..71440ad Binary files /dev/null and b/src/documentation/resources/images/apache-xml-graphics.jpg differ diff --git a/src/documentation/resources/images/apache-xml-graphics.png b/src/documentation/resources/images/apache-xml-graphics.png new file mode 100644 index 0000000..70eb879 Binary files /dev/null and b/src/documentation/resources/images/apache-xml-graphics.png differ diff --git a/src/documentation/resources/images/apache-xml-graphics.svg b/src/documentation/resources/images/apache-xml-graphics.svg new file mode 100644 index 0000000..982e6f3 --- /dev/null +++ b/src/documentation/resources/images/apache-xml-graphics.svg @@ -0,0 +1,580 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The Apache + + + + + + + + XML Graphics Project + + + + + <?xml version + <!DOCTYPE + + + + + + + http://xmlgraphics.apache.org/ + + diff --git a/src/documentation/resources/images/feed-icon-14x14.png b/src/documentation/resources/images/feed-icon-14x14.png new file mode 100644 index 0000000..b3c949d Binary files /dev/null and b/src/documentation/resources/images/feed-icon-14x14.png differ diff --git a/src/documentation/resources/stylesheets/changes2document.xsl b/src/documentation/resources/stylesheets/changes2document.xsl new file mode 100644 index 0000000..8ac82a9 --- /dev/null +++ b/src/documentation/resources/stylesheets/changes2document.xsl @@ -0,0 +1,173 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + <xsl:choose> + <xsl:when test="@title!=''"> + <xsl:value-of select="@title"/> + </xsl:when> + <xsl:otherwise> + <xsl:text>History of Changes</xsl:text> <xsl:value-of select="$versionNumber"/> + </xsl:otherwise> + </xsl:choose> + +
+ + + + + + + + + + + + +
+
+ + + + +
+ Version <xsl:value-of select="@version"/> (<xsl:value-of select="@date"/>) + + +
+ + + <xsl:choose> + <xsl:when test="//contexts/context[@id=$context]"> + <xsl:value-of select="//contexts/context[@id=$context]/@title"/> + </xsl:when> + <xsl:otherwise> + <xsl:value-of select="@context"/> + </xsl:otherwise> + </xsl:choose> + +
    + + + +
+
+
+
+
+ + +
  • + + + () + + + Thanks to + + + + + + + + + + + . + + + + Fixes + + + + + . + +
  • +
    + + + + + + + + + + + , + + + + + + + + + + + + + + + + +
    diff --git a/src/documentation/resources/stylesheets/dotdots.xsl b/src/documentation/resources/stylesheets/dotdots.xsl new file mode 100644 index 0000000..af854de --- /dev/null +++ b/src/documentation/resources/stylesheets/dotdots.xsl @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + ../ + + + + + + + + + + diff --git a/src/documentation/resources/stylesheets/news2document.xsl b/src/documentation/resources/stylesheets/news2document.xsl new file mode 100644 index 0000000..bfe91cd --- /dev/null +++ b/src/documentation/resources/stylesheets/news2document.xsl @@ -0,0 +1,53 @@ + + + + + + + + + +
    + News +
    + + + +
    +
    + + +
    + + <xsl:value-of select="date:day-in-month(@date)"/> + <xsl:text> </xsl:text> + <xsl:value-of select="date:month-abbreviation(@date)"/> + <xsl:text> </xsl:text> + <xsl:value-of select="date:year(@date)"/> + <xsl:text>: </xsl:text> + <xsl:value-of select="@title"/> + + +
    +
    + +
    diff --git a/src/documentation/resources/stylesheets/news2rss.xsl b/src/documentation/resources/stylesheets/news2rss.xsl new file mode 100644 index 0000000..252d5ab --- /dev/null +++ b/src/documentation/resources/stylesheets/news2rss.xsl @@ -0,0 +1,58 @@ + + + + + + + + + + <xsl:value-of select="$project-name"/> News + + + Subproject News for + + en + + + + + + + + <xsl:value-of select="date:day-in-month(@date)"/> + <xsl:text> </xsl:text> + <xsl:value-of select="date:month-abbreviation(@date)"/> + <xsl:text> </xsl:text> + <xsl:value-of select="date:year(@date)"/> + <xsl:text>: </xsl:text> + <xsl:value-of select="@title"/> + + news- + /index.html#news- + + + + + + + diff --git a/src/documentation/resources/stylesheets/releaseNotes2document.xsl b/src/documentation/resources/stylesheets/releaseNotes2document.xsl new file mode 100644 index 0000000..700f5e3 --- /dev/null +++ b/src/documentation/resources/stylesheets/releaseNotes2document.xsl @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + <xsl:choose> + <xsl:when test="@title!=''"> + <xsl:value-of select="@title"/> + </xsl:when> + <xsl:otherwise> + <xsl:text>Release Notes for Apache Forrest </xsl:text><xsl:value-of select="$versionNumber"/> + </xsl:otherwise> + </xsl:choose> + +
    + + + Version is a development release, + these notes are therefore not complete, they are intended to be an indicator + of the major features that are so far included in this version. + + + + + + + + +
    +
    + + +
    + Major Changes in Version <xsl:value-of select="@version"/> + This is not a complete list of changes, a + full list of changes in this release + is available. + +
    + Important Changes Code Base +
      + + + +
    +
    +
    + +
    + Important Changes Documentation +
      + + + +
    +
    +
    + +
    + Important Changes Project Administration +
      + + + +
    +
    +
    + +
    + Important Changes Design +
      + + + +
    +
    +
    + +
    + Important Changes Build +
      + + + +
    +
    +
    +
    +
    + +
    diff --git a/src/documentation/sitemap.xmap b/src/documentation/sitemap.xmap new file mode 100644 index 0000000..e63e458 --- /dev/null +++ b/src/documentation/sitemap.xmap @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/documentation/skinconf.xml b/src/documentation/skinconf.xml new file mode 100644 index 0000000..a1e2958 --- /dev/null +++ b/src/documentation/skinconf.xml @@ -0,0 +1,380 @@ + + + + + + + + + + + + true + + false + + true + + true + + + true + + + true + + + true + + + false + .at. + + + false + + + Apache™ XML Graphics Commons + Common components for Apache Batik and Apache FOP + http://xmlgraphics.apache.org/commons/ + + + + + xmlgraphics.apache.org + Apache XML Graphics is responsible for the creation and maintenance of software for managing the conversion of XML formats to graphical output, and the creation and maintenance of related software components, based on software licensed to the Foundation + http://xmlgraphics.apache.org/ + + images/apache-xml-graphics.gif + + + + + + + favicon.ico + + + 2006-2012 + The Apache Software Foundation. + + http://www.apache.org/licenses/ + + Apache, Apache XML Graphics Commons, and the Apache feather logo are trademarks of The Apache Software Foundation. All other marks mentioned may be trademarks or registered trademarks of their respective owners. + + + + + + + + + + + + + + + + + + + p.quote { + margin-left: 2em; + padding: .5em; + background-color: #f0f0f0; + font-family: monospace; + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + 0.5in + 0.5in + 1in + 0.5in + + + + false + + + false + + + + + + Built with Apache Forrest + http://forrest.apache.org/ + images/built-with-forrest-button.png + 88 + 31 + + + + + + PDF created by Apache FOP + http://xmlgraphics.apache.org/fop/ + images/logo.jpg + 138 + 31 + + + + diff --git a/src/documentation/translations/langcode.xml b/src/documentation/translations/langcode.xml new file mode 100644 index 0000000..63041b5 --- /dev/null +++ b/src/documentation/translations/langcode.xml @@ -0,0 +1,26 @@ + + + + + English + Espanol + Italiano + diff --git a/src/documentation/translations/languages_en.xml b/src/documentation/translations/languages_en.xml new file mode 100644 index 0000000..c8b239a --- /dev/null +++ b/src/documentation/translations/languages_en.xml @@ -0,0 +1,22 @@ + + + + English + Spanish + Dutch + diff --git a/src/documentation/translations/languages_es.xml b/src/documentation/translations/languages_es.xml new file mode 100644 index 0000000..2dce0cd --- /dev/null +++ b/src/documentation/translations/languages_es.xml @@ -0,0 +1,22 @@ + + + + Ingles + Espanol + Holandes + diff --git a/src/documentation/translations/menu.xml b/src/documentation/translations/menu.xml new file mode 100644 index 0000000..7d5e504 --- /dev/null +++ b/src/documentation/translations/menu.xml @@ -0,0 +1,33 @@ + + + + About + Index + Changes + Todo + Samples + Apache document + Static content + Linking + Wiki page + Ihtml page + Ehtml page + FAQ + Simplifed Docbook + XSP page + diff --git a/src/documentation/translations/menu_af.xml b/src/documentation/translations/menu_af.xml new file mode 100644 index 0000000..5444ee6 --- /dev/null +++ b/src/documentation/translations/menu_af.xml @@ -0,0 +1,33 @@ + + + + Aangaande + Inhoud + Veranderinge + Om te doen + Voorbeelde + Apache dokument + Statise Inhoud + Linking + Wiki bladsy + Ihtml bladsy + Ehtml bladsy + FAQ + Vereenvoudigde Docbook + XSP bladsy + diff --git a/src/documentation/translations/menu_de.xml b/src/documentation/translations/menu_de.xml new file mode 100644 index 0000000..237c1f6 --- /dev/null +++ b/src/documentation/translations/menu_de.xml @@ -0,0 +1,33 @@ + + + + Über + Index + Änderungen + Todo + Beispiele + Apache Dokumentationsseite + Statischer Inhalt + Linking + Wiki Seite + ihtml Seite + ehtml Seite + FAQ + Vereinfachte Docbook + XSP Seite + diff --git a/src/documentation/translations/menu_es.xml b/src/documentation/translations/menu_es.xml new file mode 100644 index 0000000..e704b9b --- /dev/null +++ b/src/documentation/translations/menu_es.xml @@ -0,0 +1,33 @@ + + + + Acerca de + Indice + Cambios + Tareas pendientes + Ejemplos + Documento Apache + Contenido Estático + Linking + Página Wiki + Página ihtml + Página ehtml + Preguntas Frecuentes + Página Simplifed Docbook + Página XSP + diff --git a/src/documentation/translations/menu_it.xml b/src/documentation/translations/menu_it.xml new file mode 100644 index 0000000..d2233aa --- /dev/null +++ b/src/documentation/translations/menu_it.xml @@ -0,0 +1,33 @@ + + + + Riguardo a + Indice + Cambiamenti + Cose da fare + Esempi + Apache document + Contenuto Statico + Linking + Pagina Wiki + Pagina ihtml + Pagina ehtml + Domande frequenti + Simplifed Docbook + Pagina XSP + diff --git a/src/documentation/translations/menu_no.xml b/src/documentation/translations/menu_no.xml new file mode 100644 index 0000000..f9be343 --- /dev/null +++ b/src/documentation/translations/menu_no.xml @@ -0,0 +1,33 @@ + + + + Om + Indeks + Endringer + Oppgave liste + Eksempler + Apache Dokument + Statisk innhold + Linking + Wiki side + ihtml side + ehtml side + FAQ + Simplifed Docbook + XSP side + diff --git a/src/documentation/translations/menu_ru.xml b/src/documentation/translations/menu_ru.xml new file mode 100644 index 0000000..0e35206 --- /dev/null +++ b/src/documentation/translations/menu_ru.xml @@ -0,0 +1,33 @@ + + + + О проекте + Содержание + Изменения + План + Примеры + Страница документа Apache + Статическое содержание + Linking + Страница Wiki + Страница ihtml + Страница ehtml + Вопросы/Ответы + Docbook страница + XSP страница + diff --git a/src/documentation/translations/menu_sk.xml b/src/documentation/translations/menu_sk.xml new file mode 100644 index 0000000..2fc3fe9 --- /dev/null +++ b/src/documentation/translations/menu_sk.xml @@ -0,0 +1,33 @@ + + + + O programe + Zoznám + Zmeny + Úlohy + Príklady + Apache Document + Statický Obsah + Linking + Wiki stránka + ihtml stránka + ehtml stránka + Casté Otázky + Simplifed Docbook stránka + XSP stránka + diff --git a/src/documentation/translations/tabs.xml b/src/documentation/translations/tabs.xml new file mode 100644 index 0000000..a5e1003 --- /dev/null +++ b/src/documentation/translations/tabs.xml @@ -0,0 +1,22 @@ + + + + Home + Samples + Apache XML Projects + diff --git a/src/documentation/translations/tabs_es.xml b/src/documentation/translations/tabs_es.xml new file mode 100644 index 0000000..d273249 --- /dev/null +++ b/src/documentation/translations/tabs_es.xml @@ -0,0 +1,22 @@ + + + + Inicio + Ejemplos + Projectos XML Apache + diff --git a/src/main/java/org/apache/xmlgraphics/fonts/Glyphs.java b/src/main/java/org/apache/xmlgraphics/fonts/Glyphs.java new file mode 100644 index 0000000..6d2fea9 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/fonts/Glyphs.java @@ -0,0 +1,841 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: Glyphs.java 1804124 2017-08-04 14:13:54Z ssteiner $ */ + +package org.apache.xmlgraphics.fonts; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.StringTokenizer; + +import org.apache.commons.io.IOUtils; + +/** + * This class provides a number of constants for glyph management. + */ +public final class Glyphs { + + private Glyphs() { + } + + /** + * Glyph name for the "notdef" glyph + */ + public static final String NOTDEF = ".notdef"; + + /** + * Glyph names for Mac encoding + * @deprecated That array was supposed to represent the standard Macintosh ordering + * of glyphs in a TrueType font (it does NOT correspond to the MacRoman encoding). + * In addition some entries are incorrect. + */ + @Deprecated + public static final String[] MAC_GLYPH_NAMES = { + /* 0x00 */ + Glyphs.NOTDEF, ".null", "CR", "space", "exclam", "quotedbl", "numbersign", + "dollar", "percent", "ampersand", "quotesingle", "parenleft", + "parenright", "asterisk", "plus", "comma", /* 0x10 */ + "hyphen", "period", "slash", "zero", "one", "two", "three", "four", + "five", "six", "seven", "eight", "nine", "colon", + "semicolon", "less", /* 0x20 */ + "equal", "greater", "question", "at", "A", "B", "C", "D", "E", "F", + "G", "H", "I", "J", "K", "L", /* 0x30 */ + "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", + "bracketleft", "backslash", /* 0x40 */ + "bracketright", "asciicircum", "underscore", "grave", "a", "b", "c", + "d", "e", "f", "g", "h", "i", "j", "k", "l", + /* 0x50 */ + "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", + "braceleft", "bar", /* 0x60 */ + "braceright", "asciitilde", "Adieresis", "Aring", "Ccedilla", + "Eacute", "Ntilde", "Odieresis", "Udieresis", "aacute", + "agrave", "acircumflex", "adieresis", "atilde", + "aring", "ccedilla", /* 0x70 */ + "eacute", "egrave", "ecircumflex", "edieresis", "iacute", "igrave", + "icircumflex", "idieresis", "ntilde", "oacute", "ograve", + "ocircumflex", "odieresis", "otilde", "uacute", "ugrave", + /* 0x80 */ + "ucircumflex", "udieresis", "dagger", "degree", "cent", "sterling", + "section", "bullet", "paragraph", "germandbls", + "registered", "copyright", "trademark", "acute", + "dieresis", "notequal", /* 0x90 */ + "AE", "Oslash", "infinity", "plusminus", "lessequal", "greaterequal", + "yen", "mu", "partialdiff", "Sigma", "Pi", "pi", "integral", + "ordfeminine", "ordmasculine", "Omega", /* 0xa0 */ + "ae", "oslash", "questiondown", "exclamdown", "logicalnot", + "radical", "florin", "approxequal", "Delta", "guillemotleft", + "guillemotright", "ellipsis", "nbspace", "Agrave", "Atilde", + "Otilde", /* 0xb0 */ + "OE", "oe", "endash", "emdash", "quotedblleft", "quotedblright", + "quoteleft", "quoteright", "divide", "lozenge", "ydieresis", + "Ydieresis", "fraction", "currency", "guilsinglleft", + "guilsinglright", /* 0xc0 */ + "fi", "fl", "daggerdbl", "periodcentered", "quotesinglbase", + "quotedblbase", "perthousand", "Acircumflex", "Ecircumflex", + "Aacute", "Edieresis", "Egrave", "Iacute", "Icircumflex", + "Idieresis", "Igrave", /* 0xd0 */ + "Oacute", "Ocircumflex", "applelogo", "Ograve", "Uacute", + "Ucircumflex", "Ugrave", "dotlessi", "circumflex", "tilde", + "macron", "breve", "dotaccent", "ring", "cedilla", + "hungarumlaut", /* 0xe0 */ + "ogonek", "caron", "Lslash", "lslash", "Scaron", "scaron", "Zcaron", + "zcaron", "brokenbar", "Eth", "eth", "Yacute", "yacute", + "Thorn", "thorn", "minus", /* 0xf0 */ + "multiply", "onesuperior", "twosuperior", "threesuperior", "onehalf", + "onequarter", "threequarters", "franc", "Gbreve", + "gbreve", "Idot", "Scedilla", "scedilla", "Cacute", + "cacute", "Ccaron", /* 0x100 */ + "ccaron", "dmacron" + }; + + /** + * Glyph names for tex8r encoding + */ + public static final String[] TEX8R_GLYPH_NAMES = { + // 0x00 + NOTDEF, "dotaccent", "fi", "fl", "fraction", "hungarumlaut", + "Lslash", "lslash", "ogonek", "ring", ".notdef", "breve", + "minus", ".notdef", "Zcaron", "zcaron", // 0x10 + "caron", "dotlessi", "dotlessj", "ff", "ffi", "ffl", ".notdef", + ".notdef", NOTDEF, NOTDEF, NOTDEF, NOTDEF, + NOTDEF, NOTDEF, "grave", "quotesingle", // 0x20 + "space", "exclam", "quotedbl", "numbersign", "dollar", "percent", + "ampersand", "quoteright", "parenleft", "parenright", + "asterisk", "plus", "comma", "hyphen", "period", "slash", + // 0x30 + "zero", "one", "two", "three", "four", "five", "six", "seven", + "eight", "nine", "colon", "semicolon", "less", "equal", + "greater", "question", // 0x40 + "at", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", + "M", "N", "O", // 0x50 + "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "bracketleft", + "backslash", "bracketright", "asciicircum", "underscore", // 0x60 + "quoteleft", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", + "l", "m", "n", "o", // 0x70 + "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "braceleft", + "bar", "braceright", "asciitilde", NOTDEF, // 0x80 + "Euro", NOTDEF, "quotesinglbase", "florin", "quotedblbase", + "ellipsis", "dagger", "daggerdbl", "circumflex", + "perthousand", "Scaron", "guilsinglleft", "OE", NOTDEF, + NOTDEF, NOTDEF, // 0x90 + NOTDEF, NOTDEF, NOTDEF, "quotedblleft", "quotedblright", + "bullet", "endash", "emdash", "tilde", "trademark", + "scaron", "guilsinglright", "oe", NOTDEF, NOTDEF, + "Ydieresis", // 0xA0 + NOTDEF, "exclamdown", "cent", "sterling", "currency", "yen", + "brokenbar", "section", "dieresis", "copyright", + "ordfeminine", "guillemotleft", "logicalnot", "hyphen", + "registered", "macron", // 0xB0 + "degree", "plusminus", "twosuperior", "threesuperior", "acute", "mu", + "paragraph", "periodcentered", "cedilla", "onesuperior", + "ordmasculine", "guillemotright", "onequarter", "onehalf", + "threequarters", "questiondown", // 0xC0 + "Agrave", "Aacute", "Acircumflex", "Atilde", "Adieresis", "Aring", + "AE", "Ccedilla", "Egrave", "Eacute", "Ecircumflex", + "Edieresis", "Igrave", "Iacute", "Icircumflex", + "Idieresis", // 0xD0 + "Eth", "Ntilde", "Ograve", "Oacute", "Ocircumflex", "Otilde", + "Odieresis", "multiply", "Oslash", "Ugrave", "Uacute", + "Ucircumflex", "Udieresis", "Yacute", "Thorn", "germandbls", + // 0xE0 + "agrave", "aacute", "acircumflex", "atilde", "adieresis", "aring", + "ae", "ccedilla", "egrave", "eacute", "ecircumflex", + "edieresis", "igrave", "iacute", "icircumflex", + "idieresis", // 0xF0 + "eth", "ntilde", "ograve", "oacute", "ocircumflex", "otilde", + "odieresis", "divide", "oslash", "ugrave", "uacute", + "ucircumflex", "udieresis", "yacute", "thorn", "ydieresis" + }; + + /** + * The characters in WinAnsiEncoding + */ + public static final char[] WINANSI_ENCODING = { + // not used until char 32 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x20 + ' ', '\u0021', '\"', '\u0023', '$', '%', '&', '\'', '(', ')', '*', '+', ',', + '\u002d', '\u002e', '/', // 0x30 + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', + '>', '?', '@', // 0x40 + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', + 'O', // 0x50 + 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '\u005b', '\\', + '\u005d', '^', '_', // 0x60 + '\u2018', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', + 'n', 'o', // 0x70 + 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '\u007b', '\u007c', '\u007d', + '\u007e', '\u2022', // 0x80 + '\u20ac', '\u2022', '\u201a', '\u0192', '\u201e', '\u2026', '\u2020', + '\u2021', '\u02c6', '\u2030', '\u0160', '\u2039', '\u0152', '\u2022', + '\u017d', '\u2022', // 0x90 + '\u2022', '\u2018', // quoteleft + '\u2019', // quoteright + '\u201c', // quotedblleft + '\u201d', // quotedblright + '\u2022', // bullet + '\u2013', // endash + '\u2014', // emdash + '~', + '\u2122', // trademark + '\u0161', '\u203a', '\u0153', '\u2022', '\u017e', '\u0178', // 0xA0 + ' ', '\u00a1', '\u00a2', '\u00a3', '\u00a4', '\u00a5', + '\u00a6', '\u00a7', '\u00a8', '\u00a9', '\u00aa', '\u00ab', + '\u00ac', '\u00ad', '\u00ae', '\u00af', // 0xb0 + '\u00b0', '\u00b1', '\u00b2', '\u00b3', '\u00b4', + '\u00b5', // This is hand-coded, the rest is assumption + '\u00b6', // and *might* not be correct... + '\u00b7', '\u00b8', '\u00b9', '\u00ba', '\u00bb', '\u00bc', '\u00bd', + '\u00be', '\u00bf', // 0xc0 + '\u00c0', '\u00c1', '\u00c2', '\u00c3', '\u00c4', '\u00c5', // Aring + '\u00c6', // AE + '\u00c7', '\u00c8', '\u00c9', '\u00ca', '\u00cb', '\u00cc', + '\u00cd', '\u00ce', '\u00cf', // 0xd0 + '\u00d0', '\u00d1', '\u00d2', '\u00d3', '\u00d4', '\u00d5', + '\u00d6', '\u00d7', '\u00d8', // Oslash + '\u00d9', '\u00da', '\u00db', '\u00dc', '\u00dd', '\u00de', + '\u00df', // 0xe0 + '\u00e0', '\u00e1', '\u00e2', '\u00e3', '\u00e4', '\u00e5', // aring + '\u00e6', // ae + '\u00e7', '\u00e8', '\u00e9', '\u00ea', '\u00eb', '\u00ec', + '\u00ed', '\u00ee', '\u00ef', // 0xf0 + '\u00f0', '\u00f1', '\u00f2', '\u00f3', '\u00f4', '\u00f5', + '\u00f6', '\u00f7', '\u00f8', '\u00f9', '\u00fa', '\u00fb', + '\u00fc', '\u00fd', '\u00fe', '\u00ff' + }; + + /** + * The characters in AdobeStandardCyrillicEncoding + */ + public static final char[] ADOBECYRILLIC_ENCODING = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ' ', //space + '\u0021', //exclam + '\"', //quotedbl + '\u0023', //numbersign + '$', //dollar + '%', //percent + '&', //ampersand + '\'', //quotesingle + '(', //parenleft + ')', //parenright + '*', //asterisk + '+', //plus + ',', //comma + '\u002d', //hyphen + '\u002e', //period + '/', //slash + + '0', //zero + '1', //one + '2', //two + '3', //three + '4', //four + '5', //five + '6', //six + '7', //seven + '8', //eight + '9', //nine + ':', //colon + ';', //semicolon + '<', //less + '=', //equal + '>', //greater + '?', //question + + '@', //at + 'A', //A + 'B', //B + 'C', //C + 'D', //D + 'E', //E + 'F', //F + 'G', //G + 'H', //H + 'I', //I + 'J', //J + 'K', //K + 'L', //L + 'M', //M + 'N', //N + 'O', //O + + 'P', //P + 'Q', //Q + 'R', //R + 'S', //S + 'T', //T + 'U', //U + 'V', //V + 'W', //W + 'X', //X + 'Y', //Y + 'Z', //Z + '\u005b', //bracketleft + '\\', //backslash + '\u005d', //bracketright + '^', //asciicircum + '_', //underscore + + '\u0060', //grave + 'a', //a + 'b', //b + 'c', //c + 'd', //d + 'e', //e + 'f', //f + 'g', //g + 'h', //h + 'i', //i + 'j', //j + 'k', //k + 'l', //l + 'm', //m + 'n', //n + 'o', //o + + 'p', //p + 'q', //q + 'r', //r + 's', //s + 't', //t + 'u', //u + 'v', //v + 'w', //w + 'x', //x + 'y', //y + 'z', //z + '\u007b', //braceleft + '\u007c', //bar + '\u007d', //braceright + '\u007e', //asciitilde + 0, + + '\u0402', //afii10051 + '\u0403', //afii10052 + '\u201a', //quotesinglebase + '\u0453', //afii10100 + '\u201e', //quotedblbase + '\u2026', //ellipsis + '\u2020', //dagger + '\u2021', //daggerdbl + '\u20ac', //euro + '\u2030', //perthousand + '\u0409', //afii10058 + '\u2039', //guilsignlleft + '\u040a', //afii10059 + '\u040c', //afii10061 + '\u040b', //afii10060 + '\u040f', //afii10045 + + '\u0452', //afii10099 + '\u2018', //quoteleft + '\u2019', //quoteright + '\u201c', //quotedblleft + '\u201d', //quotedblright + '\u2022', //bullet + '\u2013', //endash + '\u2014', //emdash + 0, + '\u2122', //trademark + '\u0459', //afii10106 + '\u203a', //guilsinglright + '\u045a', //afii10107 + '\u045c', //afii10109 + '\u045b', //afii10108 + '\u045f', //afii10193 + + '\u00a0', //nbspace + '\u040e', //afii10062 + '\u045e', //afii10110 + '\u0408', //afii10057 + '\u00a4', //currency + '\u0490', //afii10050 + '\u00a6', //brokenbar + '\u00a7', //section + '\u0401', //afii10023 + '\u00a9', //copyright + '\u0404', //afii10053 + '\u00ab', //guillemotleft + '\u00ac', //logicalnot + '\u00ad', //softhyphen + '\u00ae', //registered + '\u0407', //afii10056 + + '\u00b0', //degree + '\u00b1', //plusminus + '\u0406', //afii10055 + '\u0456', //afii10103 + '\u0491', //afii10098 + '\u00b5', //mu + '\u00b6', //paragraph + '\u00b7', //periodcentered + '\u0451', //afii10071 + '\u2116', //afii61352 + '\u0454', //afii10101 + '\u00bb', //guillemotright + '\u0458', //afii10105 + '\u0405', //afii10054 + '\u0455', //afii10102 + '\u0457', //afii10104 + + '\u0410', //afii10017 + '\u0411', //afii10018 + '\u0412', //afii10019 + '\u0413', //afii10020 + '\u0414', //afii10021 + '\u0415', //afii10022 + '\u0416', //afii10024 + '\u0417', //afii10025 + '\u0418', //afii10026 + '\u0419', //afii10027 + '\u041a', //afii10028 + '\u041b', //afii10029 + '\u041c', //afii10030 + '\u041d', //afii10031 + '\u041e', //afii10032 + '\u041f', //afii10033 + + '\u0420', //afii10034 + '\u0421', //afii10035 + '\u0422', //afii10036 + '\u0423', //afii10037 + '\u0424', //afii10038 + '\u0425', //afii10039 + '\u0426', //afii10040 + '\u0427', //afii10041 + '\u0428', //afii10042 + '\u0429', //afii10043 + '\u042a', //afii10044 + '\u042b', //afii10045 + '\u042c', //afii10046 + '\u042d', //afii10047 + '\u042e', //afii10048 + '\u042f', //afii10049 + + '\u0430', //afii10065 + '\u0431', //afii10066 + '\u0432', //afii10067 + '\u0433', //afii10068 + '\u0434', //afii10069 + '\u0435', //afii10070 + '\u0436', //afii10072 + '\u0437', //afii10073 + '\u0438', //afii10074 + '\u0439', //afii10075 + '\u043a', //afii10076 + '\u043b', //afii10077 + '\u043c', //afii10078 + '\u043d', //afii10079 + '\u043e', //afii10080 + '\u043f', //afii10081 + + '\u0440', //afii10082 + '\u0441', //afii10083 + '\u0442', //afii10084 + '\u0443', //afii10085 + '\u0444', //afii10086 + '\u0445', //afii10087 + '\u0446', //afii10088 + '\u0447', //afii10089 + '\u0448', //afii10090 + '\u0449', //afii10091 + '\u044a', //afii10092 + '\u044b', //afii10093 + '\u044c', //afii10094 + '\u044d', //afii10095 + '\u044e', //afii10096 + '\u044f', //afii10097 + }; + + /** + * List of unicode glyphs + */ + private static final String[] UNICODE_GLYPHS; + private static final String[] DINGBATS_GLYPHS; + + private static final Map CHARNAME_ALTERNATIVES; + + private static final Map CHARNAMES_TO_UNICODE; + + static { + Map map = new java.util.TreeMap(); + UNICODE_GLYPHS = loadGlyphList("glyphlist.txt", map); + DINGBATS_GLYPHS = loadGlyphList("zapfdingbats.txt", map); + CHARNAMES_TO_UNICODE = Collections.unmodifiableMap(map); + + map = new java.util.TreeMap(); + addAlternatives(map, new String[] {"Omega", "Omegagreek"}); + addAlternatives(map, new String[] {"Delta", "Deltagreek"}); + //fraction maps to 2044 (FRACTION SLASH) and 2215 (DIVISION SLASH) + addAlternatives(map, new String[] {"fraction", "divisionslash"}); + //hyphen maps to 002D (HYPHEN-MINUS) and 00AD (SOFT HYPHEN) + addAlternatives(map, new String[] {"hyphen", "sfthyphen", "softhyphen", "minus"}); + //macron maps to 00AF (MACRON) and 02C9 (MODIFIER LETTER MACRON) + addAlternatives(map, new String[] {"macron", "overscore"}); + //mu maps to 00B5 (MICRO SIGN) and 03BC (GREEK SMALL LETTER MU) + addAlternatives(map, new String[] {"mu", "mu1", "mugreek"}); + //periodcentered maps to 00B7 (MIDDLE DOT) and 2219 (BULLET OPERATOR) + addAlternatives(map, new String[] + {"periodcentered", "middot", "bulletoperator", "anoteleia"}); + //space maps to 0020 (SPACE) and 00A0 (NO-BREAK SPACE) + addAlternatives(map, new String[] {"space", "nonbreakingspace", "nbspace"}); + + //Scedilla maps to 015E (and F6C1 in private use area) + //Tcommaaccent maps to 0162 (LATIN CAPITAL LETTER T WITH CEDILLA) + // and 021a (LATIN CAPITAL LETTER T WITH COMMA BELOW) + //scedilla maps to 015f (LATIN SMALL LETTER S WITH CEDILLA) (and F6C2 in private use area) + //tcommaaccent maps to 0163 and 021b + + //map numbers from and to their respective "oldstyle" variant + addAlternatives(map, new String[] {"zero", "zerooldstyle"}); + addAlternatives(map, new String[] {"one", "oneoldstyle"}); + addAlternatives(map, new String[] {"two", "twooldstyle"}); + addAlternatives(map, new String[] {"three", "threeoldstyle"}); + addAlternatives(map, new String[] {"four", "fouroldstyle"}); + addAlternatives(map, new String[] {"five", "fiveoldstyle"}); + addAlternatives(map, new String[] {"six", "sixoldstyle"}); + addAlternatives(map, new String[] {"seven", "sevenoldstyle"}); + addAlternatives(map, new String[] {"eight", "eightoldstyle"}); + addAlternatives(map, new String[] {"nine", "nineoldstyle"}); + + //map currency signs from and to their respective "oldstyle" variant + addAlternatives(map, new String[] {"cent", "centoldstyle"}); + addAlternatives(map, new String[] {"dollar", "dollaroldstyle"}); + + //Cyrillic names according Adobe Techninal Note #5013 aka Adobe Standard Cyrillic Font Specification + addAlternatives(map, new String[] {"Acyrillic", "afii10017"}); + addAlternatives(map, new String[] {"Becyrillic", "afii10018"}); + addAlternatives(map, new String[] {"Vecyrillic", "afii10019"}); + addAlternatives(map, new String[] {"Gecyrillic", "afii10020"}); + addAlternatives(map, new String[] {"Decyrillic", "afii10021"}); + addAlternatives(map, new String[] {"Iecyrillic", "afii10022"}); + addAlternatives(map, new String[] {"Iocyrillic", "afii10023"}); + addAlternatives(map, new String[] {"Zhecyrillic", "afii10024"}); + addAlternatives(map, new String[] {"Zecyrillic", "afii10025"}); + addAlternatives(map, new String[] {"Iicyrillic", "afii10026"}); + addAlternatives(map, new String[] {"Iishortcyrillic", "afii10027"}); + addAlternatives(map, new String[] {"Kacyrillic", "afii10028"}); + addAlternatives(map, new String[] {"Elcyrillic", "afii10029"}); + addAlternatives(map, new String[] {"Emcyrillic", "afii10030"}); + addAlternatives(map, new String[] {"Encyrillic", "afii10031"}); + addAlternatives(map, new String[] {"Ocyrillic", "afii10032"}); + addAlternatives(map, new String[] {"Pecyrillic", "afii10033"}); + addAlternatives(map, new String[] {"Ercyrillic", "afii10034"}); + addAlternatives(map, new String[] {"Escyrillic", "afii10035"}); + addAlternatives(map, new String[] {"Tecyrillic", "afii10036"}); + addAlternatives(map, new String[] {"Ucyrillic", "afii10037"}); + addAlternatives(map, new String[] {"Efcyrillic", "afii10038"}); + addAlternatives(map, new String[] {"Khacyrillic", "afii10039"}); + addAlternatives(map, new String[] {"Tsecyrillic", "afii10040"}); + addAlternatives(map, new String[] {"Checyrillic", "afii10041"}); + addAlternatives(map, new String[] {"Shacyrillic", "afii10042"}); + addAlternatives(map, new String[] {"Shchacyrillic", "afii10043"}); + addAlternatives(map, new String[] {"Hardsigncyrillic", "afii10044"}); + addAlternatives(map, new String[] {"Yericyrillic", "afii10045"}); + addAlternatives(map, new String[] {"Softsigncyrillic", "afii10046"}); + addAlternatives(map, new String[] {"Ereversedcyrillic", "afii10047"}); + addAlternatives(map, new String[] {"IUcyrillic", "afii10048"}); + addAlternatives(map, new String[] {"IAcyrillic", "afii10049"}); + + addAlternatives(map, new String[] {"acyrillic", "afii10065"}); + addAlternatives(map, new String[] {"becyrillic", "afii10066"}); + addAlternatives(map, new String[] {"vecyrillic", "afii10067"}); + addAlternatives(map, new String[] {"gecyrillic", "afii10068"}); + addAlternatives(map, new String[] {"decyrillic", "afii10069"}); + addAlternatives(map, new String[] {"iecyrillic", "afii10070"}); + addAlternatives(map, new String[] {"iocyrillic", "afii10071"}); + addAlternatives(map, new String[] {"zhecyrillic", "afii10072"}); + addAlternatives(map, new String[] {"zecyrillic", "afii10073"}); + addAlternatives(map, new String[] {"iicyrillic", "afii10074"}); + addAlternatives(map, new String[] {"iishortcyrillic", "afii10075"}); + addAlternatives(map, new String[] {"kacyrillic", "afii10076"}); + addAlternatives(map, new String[] {"elcyrillic", "afii10077"}); + addAlternatives(map, new String[] {"emcyrillic", "afii10078"}); + addAlternatives(map, new String[] {"encyrillic", "afii10079"}); + addAlternatives(map, new String[] {"ocyrillic", "afii10080"}); + addAlternatives(map, new String[] {"pecyrillic", "afii10081"}); + addAlternatives(map, new String[] {"ercyrillic", "afii10082"}); + addAlternatives(map, new String[] {"escyrillic", "afii10083"}); + addAlternatives(map, new String[] {"tecyrillic", "afii10084"}); + addAlternatives(map, new String[] {"ucyrillic", "afii10085"}); + addAlternatives(map, new String[] {"efcyrillic", "afii10086"}); + addAlternatives(map, new String[] {"khacyrillic", "afii10087"}); + addAlternatives(map, new String[] {"tsecyrillic", "afii10088"}); + addAlternatives(map, new String[] {"checyrillic", "afii10089"}); + addAlternatives(map, new String[] {"shacyrillic", "afii10090"}); + addAlternatives(map, new String[] {"shchacyrillic", "afii10091"}); + addAlternatives(map, new String[] {"hardsigncyrillic", "afii10092"}); + addAlternatives(map, new String[] {"yericyrillic", "afii10093"}); + addAlternatives(map, new String[] {"softsigncyrillic", "afii10094"}); + addAlternatives(map, new String[] {"ereversedcyrillic", "afii10095"}); + addAlternatives(map, new String[] {"iucyrillic", "afii10096"}); + addAlternatives(map, new String[] {"iacyrillic", "afii10097"}); + + addAlternatives(map, new String[] {"Gheupturncyrillic", "afii10050"}); + addAlternatives(map, new String[] {"Djecyrillic", "afii10051"}); + addAlternatives(map, new String[] {"Gjecyrillic", "afii10052"}); + addAlternatives(map, new String[] {"Ecyrillic", "afii10053"}); + addAlternatives(map, new String[] {"Dzecyrillic", "afii10054"}); + addAlternatives(map, new String[] {"Icyrillic", "afii10055"}); + addAlternatives(map, new String[] {"Yicyrillic", "afii10056"}); + addAlternatives(map, new String[] {"Jecyrillic", "afii10057"}); + addAlternatives(map, new String[] {"Ljecyrillic", "afii10058"}); + addAlternatives(map, new String[] {"Njecyrillic", "afii10059"}); + addAlternatives(map, new String[] {"Tshecyrillic", "afii10060"}); + addAlternatives(map, new String[] {"Kjecyrillic", "afii10061"}); + addAlternatives(map, new String[] {"Ushortcyrillic", "afii10062"}); + + addAlternatives(map, new String[] {"Dzhecyrillic", "afii10145"}); + addAlternatives(map, new String[] {"Yatcyrillic", "afii10146"}); + addAlternatives(map, new String[] {"Fitacyrillic", "afii10147"}); + addAlternatives(map, new String[] {"Izhitsacyrillic", "afii10148"}); + + addAlternatives(map, new String[] {"gheupturncyrillic", "afii10098"}); + addAlternatives(map, new String[] {"djecyrillic", "afii10099"}); + addAlternatives(map, new String[] {"gjecyrillic", "afii10100"}); + addAlternatives(map, new String[] {"ecyrillic", "afii10101"}); + addAlternatives(map, new String[] {"dzecyrillic", "afii10102"}); + addAlternatives(map, new String[] {"icyrillic", "afii10103"}); + addAlternatives(map, new String[] {"yicyrillic", "afii10104"}); + addAlternatives(map, new String[] {"jecyrillic", "afii10105"}); + addAlternatives(map, new String[] {"ljecyrillic", "afii10106"}); + addAlternatives(map, new String[] {"njecyrillic", "afii10107"}); + addAlternatives(map, new String[] {"tshecyrillic", "afii10108"}); + addAlternatives(map, new String[] {"kjecyrillic", "afii10109"}); + addAlternatives(map, new String[] {"ushortcyrillic", "afii10110"}); + + addAlternatives(map, new String[] {"dzhecyrillic", "afii10193"}); + addAlternatives(map, new String[] {"yatcyrillic", "afii10194"}); + addAlternatives(map, new String[] {"fitacyrillic", "afii10195"}); + addAlternatives(map, new String[] {"izhitsacyrillic", "afii10196"}); + + CHARNAME_ALTERNATIVES = Collections.unmodifiableMap(map); + } + + private static void addAlternatives(Map map, String[] alternatives) { + for (int i = 0, c = alternatives.length; i < c; i++) { + String[] alt = new String[c - 1]; + int idx = 0; + for (int j = 0; j < c; j++) { + if (i != j) { + alt[idx] = alternatives[j]; + idx++; + } + } + map.put(alternatives[i], alt); + } + } + + private static String[] loadGlyphList(String filename, Map charNameToUnicodeMap) { + List lines = new java.util.ArrayList(); + InputStream in = Glyphs.class.getResourceAsStream(filename); + if (in == null) { + throw new RuntimeException("Cannot load " + filename + + ". The Glyphs class cannot properly be initialized!"); + } + try { + BufferedReader reader = new BufferedReader(new InputStreamReader(in, "US-ASCII")); + String line; + try { + while ((line = reader.readLine()) != null) { + if (!line.startsWith("#")) { + lines.add(line); + } + } + } finally { + reader.close(); + } + } catch (UnsupportedEncodingException uee) { + throw new RuntimeException("Incompatible JVM! US-ASCII encoding is not supported." + + " The Glyphs class cannot properly be initialized!"); + } catch (IOException ioe) { + throw new RuntimeException("I/O error while loading " + filename + + ". The Glyphs class cannot properly be initialized!"); + } finally { + IOUtils.closeQuietly(in); + } + String[] arr = new String[lines.size() * 2]; + int pos = 0; + StringBuffer buf = new StringBuffer(); + for (Object line1 : lines) { + String line = (String) line1; + int semicolon = line.indexOf(';'); + if (semicolon <= 0) { + continue; + } + String charName = line.substring(0, semicolon); + String rawUnicode = line.substring(semicolon + 1); + buf.setLength(0); + + StringTokenizer tokenizer = new StringTokenizer(rawUnicode, " ", false); + while (tokenizer.hasMoreTokens()) { + String token = tokenizer.nextToken(); + assert token.length() == 4; + buf.append(hexToChar(token)); + } + + String unicode = buf.toString(); + arr[pos] = unicode; + pos++; + arr[pos] = charName; + pos++; + assert !charNameToUnicodeMap.containsKey(charName); + charNameToUnicodeMap.put(charName, unicode); + } + return arr; + } + + private static char hexToChar(String hex) { + return (char)Integer.parseInt(hex, 16); + } + + /** + * Return the glyphname from a character, + * eg, charToGlyphName('\\') returns "backslash" + * + * @param ch glyph to evaluate + * @return the name of the glyph + */ + public static String charToGlyphName(char ch) { + return stringToGlyph(Character.toString(ch)); + } + + /** + * Returns a String containing the Unicode sequence the given glyph name represents. + * @param glyphName the glyph name + * @return the Unicode sequence of the glyph (or null if the glyph name is unknown) + */ + public static String getUnicodeSequenceForGlyphName(String glyphName) { + //Mapping: see http://www.adobe.com/devnet/opentype/archives/glyph.html + //Step 1 + int period = glyphName.indexOf('.'); + if (period >= 0) { + glyphName = glyphName.substring(0, period); + } + + //Step 2 + StringBuffer sb = new StringBuffer(); + StringTokenizer tokenizer = new StringTokenizer(glyphName, "_", false); + while (tokenizer.hasMoreTokens()) { + String token = tokenizer.nextToken(); + //Step 3 + String sequence = (String)CHARNAMES_TO_UNICODE.get(token); + if (sequence == null) { + if (token.startsWith("uni")) { + int len = token.length(); + int pos = 3; + while (pos + 4 <= len) { + try { + sb.append(hexToChar(token.substring(pos, pos + 4))); + } catch (NumberFormatException nfe) { + return null; + } + pos += 4; + } + } else if (token.startsWith("u")) { + if (token.length() > 5) { + //TODO: Unicode scalar values greater than FFFF are currently not supported + return null; + } + if (token.length() < 5) { + /* + * This is not in the form of 'u1234' --probably a + * non-official glyph name that isn't listed in the + * unicode map. + */ + return null; + } + try { + sb.append(hexToChar(token.substring(1, 5))); + } catch (NumberFormatException nfe) { + return null; + } + } else { + //ignore (empty string) + } + } else { + sb.append(sequence); + } + } + + if (sb.length() == 0) { + return null; + } else { + return sb.toString(); + } + } + + /** + * Return the glyphname from a string, + * eg, glyphToString("\\") returns "backslash" + * + * @param name glyph to evaluate + * @return the name of the glyph + * TODO: javadocs for glyphToString and stringToGlyph are confused + * @deprecated User getUnicodeCodePointsForGlyphName instead. This method only returns the + * first Unicode code point it finds. + */ + @Deprecated + public static String glyphToString(String name) { + for (int i = 0; i < UNICODE_GLYPHS.length; i += 2) { + if (UNICODE_GLYPHS[i + 1].equals(name)) { + return UNICODE_GLYPHS[i]; + } + } + return ""; + } + + /** + * Return the string representation of a glyphname, + * eg stringToGlyph("backslash") returns "\\" + * + * @param name name of the glyph + * @return the string representation (or an empty String if no match was found) + */ + public static String stringToGlyph(String name) { + for (int i = 0; i < UNICODE_GLYPHS.length; i += 2) { + if (UNICODE_GLYPHS[i].equals(name)) { + return UNICODE_GLYPHS[i + 1]; + } + } + for (int i = 0; i < DINGBATS_GLYPHS.length; i += 2) { + if (DINGBATS_GLYPHS[i].equals(name)) { + return DINGBATS_GLYPHS[i + 1]; + } + } + return ""; + } + + /** + * Returns an array of char names which can serve as alternatives for the given one. + * @param charName the character name to search alternatives for + * @return an array of char names or null if no alternatives are available + */ + public static String[] getCharNameAlternativesFor(String charName) { + return (String[])CHARNAME_ALTERNATIVES.get(charName); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/fonts/package.html b/src/main/java/org/apache/xmlgraphics/fonts/package.html new file mode 100644 index 0000000..9643a2a --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/fonts/package.html @@ -0,0 +1,23 @@ + + + +org.apache.xmlgraphics.fonts Package + +

    Classes for handling fonts, glyphs, font encodings etc.

    + + \ No newline at end of file diff --git a/src/main/java/org/apache/xmlgraphics/image/GraphicsConstants.java b/src/main/java/org/apache/xmlgraphics/image/GraphicsConstants.java new file mode 100644 index 0000000..4eeadff --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/GraphicsConstants.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: GraphicsConstants.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.image; + +public final class GraphicsConstants { + + /** + * The default DPI used when they cannot be determined by the graphics + * environment or loaded from the image. + */ + public static final int DEFAULT_DPI = 72; + + /** + * Graphics may be sampled at this resolution. + */ + public static final int DEFAULT_SAMPLE_DPI = 300; + + /** + * This class should not be instantiated. + */ + private GraphicsConstants() { + } +} diff --git a/src/main/java/org/apache/xmlgraphics/image/GraphicsUtil.java b/src/main/java/org/apache/xmlgraphics/image/GraphicsUtil.java new file mode 100644 index 0000000..ce04fe9 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/GraphicsUtil.java @@ -0,0 +1,1222 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: GraphicsUtil.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.image; + +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.color.ColorSpace; +import java.awt.geom.AffineTransform; +import java.awt.image.BufferedImage; +import java.awt.image.ColorModel; +import java.awt.image.ComponentSampleModel; +import java.awt.image.DataBuffer; +import java.awt.image.DataBufferByte; +import java.awt.image.DataBufferInt; +import java.awt.image.DataBufferShort; +import java.awt.image.DataBufferUShort; +import java.awt.image.DirectColorModel; +import java.awt.image.Raster; +import java.awt.image.RenderedImage; +import java.awt.image.SampleModel; +import java.awt.image.SinglePixelPackedSampleModel; +import java.awt.image.WritableRaster; + +import org.apache.xmlgraphics.image.rendered.Any2LsRGBRed; +import org.apache.xmlgraphics.image.rendered.Any2sRGBRed; +import org.apache.xmlgraphics.image.rendered.BufferedImageCachableRed; +import org.apache.xmlgraphics.image.rendered.CachableRed; +import org.apache.xmlgraphics.image.rendered.RenderedImageCachableRed; + +// CSOFF: AvoidNestedBlocks +// CSOFF: ConstantName +// CSOFF: MethodName +// CSOFF: MultipleVariableDeclarations +// CSOFF: NeedBraces +// CSOFF: OneStatementPerLine +// CSOFF: OperatorWrap +// CSOFF: StaticVariableName +// CSOFF: WhitespaceAfter +// CSOFF: WhitespaceAround + +/** + * Set of utility methods for Graphics. + * These generally bypass broken methods in Java2D or provide tweaked + * implementations. + * + * @version $Id: GraphicsUtil.java 1732018 2016-02-24 04:51:06Z gadams $ + * + * Originally authored by Thomas DeWeese. + */ +public final class GraphicsUtil { + + private GraphicsUtil() { + } + + public static final AffineTransform IDENTITY = new AffineTransform(); + + /** + * Standard prebuilt Linear_sRGB color model with no alpha */ + public static final ColorModel Linear_sRGB = + new DirectColorModel(ColorSpace.getInstance( + ColorSpace.CS_LINEAR_RGB), 24, + 0x00FF0000, 0x0000FF00, + 0x000000FF, 0x0, false, + DataBuffer.TYPE_INT); + /** + * Standard prebuilt Linear_sRGB color model with premultiplied alpha. + */ + public static final ColorModel Linear_sRGB_Pre = + new DirectColorModel(ColorSpace.getInstance( + ColorSpace.CS_LINEAR_RGB), 32, + 0x00FF0000, 0x0000FF00, + 0x000000FF, 0xFF000000, true, + DataBuffer.TYPE_INT); + /** + * Standard prebuilt Linear_sRGB color model with unpremultiplied alpha. + */ + public static final ColorModel Linear_sRGB_Unpre = + new DirectColorModel(ColorSpace.getInstance( + ColorSpace.CS_LINEAR_RGB), 32, + 0x00FF0000, 0x0000FF00, + 0x000000FF, 0xFF000000, false, + DataBuffer.TYPE_INT); + + /** + * Standard prebuilt sRGB color model with no alpha. + */ + public static final ColorModel sRGB = + new DirectColorModel(ColorSpace.getInstance( + ColorSpace.CS_sRGB), 24, + 0x00FF0000, 0x0000FF00, + 0x000000FF, 0x0, false, + DataBuffer.TYPE_INT); + /** + * Standard prebuilt sRGB color model with premultiplied alpha. + */ + public static final ColorModel sRGB_Pre = + new DirectColorModel(ColorSpace.getInstance( + ColorSpace.CS_sRGB), 32, + 0x00FF0000, 0x0000FF00, + 0x000000FF, 0xFF000000, true, + DataBuffer.TYPE_INT); + /** + * Standard prebuilt sRGB color model with unpremultiplied alpha. + */ + public static final ColorModel sRGB_Unpre = + new DirectColorModel(ColorSpace.getInstance( + ColorSpace.CS_sRGB), 32, + 0x00FF0000, 0x0000FF00, + 0x000000FF, 0xFF000000, false, + DataBuffer.TYPE_INT); + + /** + * Method that returns either Linear_sRGB_Pre or Linear_sRGB_UnPre + * based on premult flag. + * @param premult True if the ColorModel should have premultiplied alpha. + * @return a ColorMdoel with Linear sRGB colorSpace and + * the alpha channel set in accordance with + * premult + */ + public static ColorModel makeLinear_sRGBCM(boolean premult) { + return premult ? Linear_sRGB_Pre : Linear_sRGB_Unpre; + } + + /** + * Constructs a BufferedImage with a linear sRGB colorModel, and alpha. + * @param width The desired width of the BufferedImage + * @param height The desired height of the BufferedImage + * @param premult The desired state of alpha premultiplied + * @return The requested BufferedImage. + */ + public static BufferedImage makeLinearBufferedImage(int width, + int height, + boolean premult) { + ColorModel cm = makeLinear_sRGBCM(premult); + WritableRaster wr = cm.createCompatibleWritableRaster(width, height); + return new BufferedImage(cm, wr, premult, null); + } + + /** + * This method will return a CacheableRed that has it's data in + * the linear sRGB colorspace. If src is already in + * linear sRGB then this method does nothing and returns src. + * Otherwise it creates a transform that will convert + * src's output to linear sRGB and returns that CacheableRed. + * + * @param src The image to convert to linear sRGB. + * @return An equivilant image to src who's data is in + * linear sRGB. + */ + public static CachableRed convertToLsRGB(CachableRed src) { + ColorModel cm = src.getColorModel(); + ColorSpace cs = cm.getColorSpace(); + if (cs == ColorSpace.getInstance(ColorSpace.CS_LINEAR_RGB)) { + return src; + } + + return new Any2LsRGBRed(src); + } + + /** + * This method will return a CacheableRed that has it's data in + * the sRGB colorspace. If src is already in + * sRGB then this method does nothing and returns src. + * Otherwise it creates a transform that will convert + * src's output to sRGB and returns that CacheableRed. + * + * @param src The image to convert to sRGB. + * @return An equivilant image to src who's data is in sRGB. + */ + public static CachableRed convertTosRGB(CachableRed src) { + ColorModel cm = src.getColorModel(); + ColorSpace cs = cm.getColorSpace(); + if (cs == ColorSpace.getInstance(ColorSpace.CS_sRGB)) { + return src; + } + + return new Any2sRGBRed(src); + } + + /** + * Convertes any RenderedImage to a CacheableRed.

    + * If ri is already a CacheableRed it casts it down and + * returns it.

    + * + * In cases where ri is not already a CacheableRed it + * wraps ri with a helper class. The wrapped + * CacheableRed "Pretends" that it has no sources since it has no + * way of inteligently handling the dependency/dirty region calls + * if it exposed the source. + * @param ri The RenderedImage to convert. + * @return a CacheableRed that contains the same data as ri. + */ + public static CachableRed wrap(RenderedImage ri) { + if (ri instanceof CachableRed) { + return (CachableRed) ri; + } + if (ri instanceof BufferedImage) { + return new BufferedImageCachableRed((BufferedImage)ri); + } + return new RenderedImageCachableRed(ri); + } + + /** + * An internal optimized version of copyData designed to work on + * Integer packed data with a SinglePixelPackedSampleModel. Only + * the region of overlap between src and dst is copied. + * + * Calls to this should be preflighted with is_INT_PACK_Data + * on both src and dest (requireAlpha can be false). + * + * @param src The source of the data + * @param dst The destination for the data. + */ + public static void copyData_INT_PACK(Raster src, WritableRaster dst) { + // System.out.println("Fast copyData"); + int x0 = dst.getMinX(); + if (x0 < src.getMinX()) { + x0 = src.getMinX(); + } + + int y0 = dst.getMinY(); + if (y0 < src.getMinY()) { + y0 = src.getMinY(); + } + + int x1 = dst.getMinX() + dst.getWidth() - 1; + if (x1 > src.getMinX() + src.getWidth() - 1) { + x1 = src.getMinX() + src.getWidth() - 1; + } + + int y1 = dst.getMinY() + dst.getHeight() - 1; + if (y1 > src.getMinY() + src.getHeight() - 1) { + y1 = src.getMinY() + src.getHeight() - 1; + } + + int width = x1 - x0 + 1; + int height = y1 - y0 + 1; + + SinglePixelPackedSampleModel srcSPPSM; + srcSPPSM = (SinglePixelPackedSampleModel)src.getSampleModel(); + + final int srcScanStride = srcSPPSM.getScanlineStride(); + DataBufferInt srcDB = (DataBufferInt)src.getDataBuffer(); + final int [] srcPixels = srcDB.getBankData()[0]; + final int srcBase = + (srcDB.getOffset() + + srcSPPSM.getOffset(x0 - src.getSampleModelTranslateX(), + y0 - src.getSampleModelTranslateY())); + + + SinglePixelPackedSampleModel dstSPPSM; + dstSPPSM = (SinglePixelPackedSampleModel)dst.getSampleModel(); + + final int dstScanStride = dstSPPSM.getScanlineStride(); + DataBufferInt dstDB = (DataBufferInt)dst.getDataBuffer(); + final int [] dstPixels = dstDB.getBankData()[0]; + final int dstBase = + (dstDB.getOffset() + + dstSPPSM.getOffset(x0 - dst.getSampleModelTranslateX(), + y0 - dst.getSampleModelTranslateY())); + + if ((srcScanStride == dstScanStride) + && (srcScanStride == width)) { + // System.out.println("VERY Fast copyData"); + + System.arraycopy(srcPixels, srcBase, dstPixels, dstBase, + width * height); + } else if (width > 128) { + int srcSP = srcBase; + int dstSP = dstBase; + for (int y = 0; y < height; y++) { + System.arraycopy(srcPixels, srcSP, dstPixels, dstSP, width); + srcSP += srcScanStride; + dstSP += dstScanStride; + } + } else { + for (int y = 0; y < height; y++) { + int srcSP = srcBase + y * srcScanStride; + int dstSP = dstBase + y * dstScanStride; + for (int x = 0; x < width; x++) { + dstPixels[dstSP++] = srcPixels[srcSP++]; + } + } + } + } + + public static void copyData_FALLBACK(Raster src, WritableRaster dst) { + // System.out.println("Fallback copyData"); + + int x0 = dst.getMinX(); + if (x0 < src.getMinX()) { + x0 = src.getMinX(); + } + + int y0 = dst.getMinY(); + if (y0 < src.getMinY()) { + y0 = src.getMinY(); + } + + int x1 = dst.getMinX() + dst.getWidth() - 1; + if (x1 > src.getMinX() + src.getWidth() - 1) { + x1 = src.getMinX() + src.getWidth() - 1; + } + + int y1 = dst.getMinY() + dst.getHeight() - 1; + if (y1 > src.getMinY() + src.getHeight() - 1) { + y1 = src.getMinY() + src.getHeight() - 1; + } + + int width = x1 - x0 + 1; + int [] data = null; + + for (int y = y0; y <= y1; y++) { + data = src.getPixels(x0, y, width, 1, data); + dst.setPixels(x0, y, width, 1, data); + } + } + + /** + * Copies data from one raster to another. Only the region of + * overlap between src and dst is copied. Src and + * Dst must have compatible SampleModels. + * + * @param src The source of the data + * @param dst The destination for the data. + */ + public static void copyData(Raster src, WritableRaster dst) { + if (is_INT_PACK_Data(src.getSampleModel(), false) + && is_INT_PACK_Data(dst.getSampleModel(), false)) { + copyData_INT_PACK(src, dst); + return; + } + + copyData_FALLBACK(src, dst); + } + + /** + * Creates a new raster that has a copy of the data in + * ras. This is highly optimized for speed. There is + * no provision for changing any aspect of the SampleModel. + * + * This method should be used when you need to change the contents + * of a Raster that you do not "own" (ie the result of a + * getData call). + * @param ras The Raster to copy. + * @return A writable copy of ras + */ + public static WritableRaster copyRaster(Raster ras) { + return copyRaster(ras, ras.getMinX(), ras.getMinY()); + } + + + /** + * Creates a new raster that has a copy of the data in + * ras. This is highly optimized for speed. There is + * no provision for changing any aspect of the SampleModel. + * However you can specify a new location for the returned raster. + * + * This method should be used when you need to change the contents + * of a Raster that you do not "own" (ie the result of a + * getData call). + * + * @param ras The Raster to copy. + * + * @param minX The x location for the upper left corner of the + * returned WritableRaster. + * + * @param minY The y location for the upper left corner of the + * returned WritableRaster. + * + * @return A writable copy of ras + */ + public static WritableRaster copyRaster(Raster ras, int minX, int minY) { + WritableRaster ret = Raster.createWritableRaster( + ras.getSampleModel(), + new Point(0, 0)); + ret = ret.createWritableChild( + ras.getMinX() - ras.getSampleModelTranslateX(), + ras.getMinY() - ras.getSampleModelTranslateY(), + ras.getWidth(), ras.getHeight(), + minX, minY, null); + + // Use System.arraycopy to copy the data between the two... + DataBuffer srcDB = ras.getDataBuffer(); + DataBuffer retDB = ret.getDataBuffer(); + if (srcDB.getDataType() != retDB.getDataType()) { + throw new IllegalArgumentException( + "New DataBuffer doesn't match original"); + } + int len = srcDB.getSize(); + int banks = srcDB.getNumBanks(); + int [] offsets = srcDB.getOffsets(); + for (int b = 0; b < banks; b++) { + switch (srcDB.getDataType()) { + case DataBuffer.TYPE_BYTE: { + DataBufferByte srcDBT = (DataBufferByte)srcDB; + DataBufferByte retDBT = (DataBufferByte)retDB; + System.arraycopy(srcDBT.getData(b), offsets[b], + retDBT.getData(b), offsets[b], len); + break; + } + case DataBuffer.TYPE_INT: { + DataBufferInt srcDBT = (DataBufferInt)srcDB; + DataBufferInt retDBT = (DataBufferInt)retDB; + System.arraycopy(srcDBT.getData(b), offsets[b], + retDBT.getData(b), offsets[b], len); + break; + } + case DataBuffer.TYPE_SHORT: { + DataBufferShort srcDBT = (DataBufferShort)srcDB; + DataBufferShort retDBT = (DataBufferShort)retDB; + System.arraycopy(srcDBT.getData(b), offsets[b], + retDBT.getData(b), offsets[b], len); + break; + } + case DataBuffer.TYPE_USHORT: { + DataBufferUShort srcDBT = (DataBufferUShort)srcDB; + DataBufferUShort retDBT = (DataBufferUShort)retDB; + System.arraycopy(srcDBT.getData(b), offsets[b], + retDBT.getData(b), offsets[b], len); + break; + } + default: + throw new + UnsupportedOperationException("unsupported data type: " + + srcDB.getDataType()); + } + } + + return ret; + } + + /** + * Coerces ras to be writable. The returned Raster continues to + * reference the DataBuffer from ras, so modifications to the returned + * WritableRaster will be seen in ras.

    + * + * This method should only be used if you need a WritableRaster due to + * an interface (such as to construct a BufferedImage), but have no + * intention of modifying the contents of the returned Raster. If + * you have any doubt about other users of the data in ras, + * use copyRaster (above). + * @param ras The raster to make writable. + * @return A Writable version of ras (shares DataBuffer with + * ras). + */ + public static WritableRaster makeRasterWritable(Raster ras) { + return makeRasterWritable(ras, ras.getMinX(), ras.getMinY()); + } + + /** + * Coerces ras to be writable. The returned Raster continues to + * reference the DataBuffer from ras, so modifications to the returned + * WritableRaster will be seen in ras.

    + * + * You can specify a new location for the returned WritableRaster, this + * is especially useful for constructing BufferedImages which require + * the Raster to be at (0,0). + * + * This method should only be used if you need a WritableRaster due to + * an interface (such as to construct a BufferedImage), but have no + * intention of modifying the contents of the returned Raster. If + * you have any doubt about other users of the data in ras, + * use copyRaster (above). + * + * @param ras The raster to make writable. + * + * @param minX The x location for the upper left corner of the + * returned WritableRaster. + * + * @param minY The y location for the upper left corner of the + * returned WritableRaster. + * + * @return A Writable version of ras with it's upper left + * hand coordinate set to minX, minY (shares it's DataBuffer + * with ras). + */ + public static WritableRaster makeRasterWritable(Raster ras, + int minX, int minY) { + WritableRaster ret = Raster.createWritableRaster( + ras.getSampleModel(), + ras.getDataBuffer(), + new Point(0, 0)); + ret = ret.createWritableChild( + ras.getMinX() - ras.getSampleModelTranslateX(), + ras.getMinY() - ras.getSampleModelTranslateY(), + ras.getWidth(), ras.getHeight(), + minX, minY, null); + return ret; + } + + /** + * Create a new ColorModel with it's alpha premultiplied state matching + * newAlphaPreMult. + * @param cm The ColorModel to change the alpha premult state of. + * @param newAlphaPreMult The new state of alpha premult. + * @return A new colorModel that has isAlphaPremultiplied() + * equal to newAlphaPreMult. + */ + public static ColorModel + coerceColorModel(ColorModel cm, boolean newAlphaPreMult) { + if (cm.isAlphaPremultiplied() == newAlphaPreMult) { + return cm; + } + + // Easiest way to build proper colormodel for new Alpha state... + // Eventually this should switch on known ColorModel types and + // only fall back on this hack when the CM type is unknown. + WritableRaster wr = cm.createCompatibleWritableRaster(1, 1); + return cm.coerceData(wr, newAlphaPreMult); + } + + /** + * Coerces data within a bufferedImage to match newAlphaPreMult, + * Note that this can not change the colormodel of bi so you + * + * @param wr The raster to change the state of. + * @param cm The colormodel currently associated with data in wr. + * @param newAlphaPreMult The desired state of alpha Premult for raster. + * @return A new colormodel that matches newAlphaPreMult. + */ + public static ColorModel + coerceData(WritableRaster wr, ColorModel cm, boolean newAlphaPreMult) { + + // System.out.println("CoerceData: " + cm.isAlphaPremultiplied() + + // " Out: " + newAlphaPreMult); + if (!cm.hasAlpha()) { + // Nothing to do no alpha channel + return cm; + } + + if (cm.isAlphaPremultiplied() == newAlphaPreMult) { + // nothing to do alpha state matches... + return cm; + } + + // System.out.println("CoerceData: " + wr.getSampleModel()); + + if (newAlphaPreMult) { + multiplyAlpha(wr); + } else { + divideAlpha(wr); + } + + return coerceColorModel(cm, newAlphaPreMult); + } + + public static void multiplyAlpha(WritableRaster wr) { + if (is_BYTE_COMP_Data(wr.getSampleModel())) { + mult_BYTE_COMP_Data(wr); + } else if (is_INT_PACK_Data(wr.getSampleModel(), true)) { + mult_INT_PACK_Data(wr); + } else { + int [] pixel = null; + int bands = wr.getNumBands(); + float norm = 1f / 255f; + int x0; + int x1; + int y0; + int y1; + int a; + int b; + float alpha; + x0 = wr.getMinX(); + x1 = x0 + wr.getWidth(); + y0 = wr.getMinY(); + y1 = y0 + wr.getHeight(); + for (int y = y0; y < y1; y++) { + for (int x = x0; x < x1; x++) { + pixel = wr.getPixel(x, y, pixel); + a = pixel[bands - 1]; + if ((a >= 0) && (a < 255)) { + alpha = a * norm; + for (b = 0; b < bands - 1; b++) { + pixel[b] = (int)(pixel[b] * alpha + 0.5f); + } + wr.setPixel(x, y, pixel); + } + } + } + } + } + + public static void divideAlpha(WritableRaster wr) { + if (is_BYTE_COMP_Data(wr.getSampleModel())) { + divide_BYTE_COMP_Data(wr); + } else if (is_INT_PACK_Data(wr.getSampleModel(), true)) { + divide_INT_PACK_Data(wr); + } else { + int x0; + int x1; + int y0; + int y1; + int a; + int b; + float ialpha; + int bands = wr.getNumBands(); + int [] pixel = null; + + x0 = wr.getMinX(); + x1 = x0 + wr.getWidth(); + y0 = wr.getMinY(); + y1 = y0 + wr.getHeight(); + for (int y = y0; y < y1; y++) { + for (int x = x0; x < x1; x++) { + pixel = wr.getPixel(x, y, pixel); + a = pixel[bands - 1]; + if ((a > 0) && (a < 255)) { + ialpha = 255 / (float)a; + for (b = 0; b < bands - 1; b++) { + pixel[b] = (int)(pixel[b] * ialpha + 0.5f); + } + wr.setPixel(x, y, pixel); + } + } + } + } + } + + /** + * Copies data from one bufferedImage to another paying attention + * to the state of AlphaPreMultiplied. + * + * @param src The source + * @param dst The destination + */ + public static void + copyData(BufferedImage src, BufferedImage dst) { + Rectangle srcRect = new Rectangle(0, 0, + src.getWidth(), src.getHeight()); + copyData(src, srcRect, dst, new Point(0, 0)); + } + + + /** + * Copies data from one bufferedImage to another paying attention + * to the state of AlphaPreMultiplied. + * + * @param src The source + * @param srcRect The Rectangle of source data to be copied + * @param dst The destination + * @param destP The Place for the upper left corner of srcRect in dst. + */ + public static void + copyData(BufferedImage src, Rectangle srcRect, + BufferedImage dst, Point destP) { + + /* + if (srcCS != dstCS) + throw new IllegalArgumentException + ("Images must be in the same ColorSpace in order "+ + "to copy Data between them"); + */ + boolean srcAlpha = src.getColorModel().hasAlpha(); + boolean dstAlpha = dst.getColorModel().hasAlpha(); + + // System.out.println("Src has: " + srcAlpha + + // " is: " + src.isAlphaPremultiplied()); + // + // System.out.println("Dst has: " + dstAlpha + + // " is: " + dst.isAlphaPremultiplied()); + + if (srcAlpha == dstAlpha) { + if (!srcAlpha + || src.isAlphaPremultiplied() == dst.isAlphaPremultiplied()) { + // They match one another so just copy everything... + copyData(src.getRaster(), dst.getRaster()); + return; + } + } + + // System.out.println("Using Slow CopyData"); + + int [] pixel = null; + Raster srcR = src.getRaster(); + WritableRaster dstR = dst.getRaster(); + int bands = dstR.getNumBands(); + + int dx = destP.x - srcRect.x; + int dy = destP.y - srcRect.y; + + int w = srcRect.width; + int x0 = srcRect.x; + int y0 = srcRect.y; + int y1 = y0 + srcRect.height - 1; + + if (!srcAlpha) { + // Src has no alpha dest does so set alpha to 1.0 everywhere. + // System.out.println("Add Alpha"); + int [] oPix = new int[bands * w]; + int out = (w * bands) - 1; // The 2 skips alpha channel + while (out >= 0) { + // Fill alpha channel with 255's + oPix[out] = 255; + out -= bands; + } + + int b; + int in; + for (int y = y0; y <= y1; y++) { + pixel = srcR.getPixels(x0, y, w, 1, pixel); + in = w * (bands - 1) - 1; + out = (w * bands) - 2; // The 2 skips alpha channel on last pix + switch (bands) { + case 4: + while (in >= 0) { + oPix[out--] = pixel[in--]; + oPix[out--] = pixel[in--]; + oPix[out--] = pixel[in--]; + out--; + } + break; + default: + while (in >= 0) { + for (b = 0; b < bands - 1; b++) { + oPix[out--] = pixel[in--]; + } + out--; + } + } + dstR.setPixels(x0 + dx, y + dy, w, 1, oPix); + } + } else if (dstAlpha && dst.isAlphaPremultiplied()) { + // Src and dest have Alpha but we need to multiply it for dst. + // System.out.println("Mult Case"); + int a; + int b; + int alpha; + int in; + int fpNorm = (1 << 24) / 255; + int pt5 = 1 << 23; + for (int y = y0; y <= y1; y++) { + pixel = srcR.getPixels(x0, y, w, 1, pixel); + in = bands * w - 1; + switch (bands) { + case 4: + while (in >= 0) { + a = pixel[in]; + if (a == 255) { + in -= 4; + } else { + in--; + alpha = fpNorm * a; + pixel[in] = (pixel[in] * alpha + pt5) >>> 24; + in--; + pixel[in] = (pixel[in] * alpha + pt5) >>> 24; + in--; + pixel[in] = (pixel[in] * alpha + pt5) >>> 24; + in--; + } + } + break; + default: + while (in >= 0) { + a = pixel[in]; + if (a == 255) { + in -= bands; + } else { + in--; + alpha = fpNorm * a; + for (b = 0; b < bands - 1; b++) { + pixel[in] = (pixel[in] * alpha + pt5) >>> 24; + in--; + } + } + } + } + dstR.setPixels(x0 + dx, y + dy, w, 1, pixel); + } + } else if (dstAlpha && !dst.isAlphaPremultiplied()) { + // Src and dest have Alpha but we need to divide it out for dst. + // System.out.println("Div Case"); + int a; + int b; + int ialpha; + int in; + int fpNorm = 0x00FF0000; + int pt5 = 1 << 15; + for (int y = y0; y <= y1; y++) { + pixel = srcR.getPixels(x0, y, w, 1, pixel); + in = (bands * w) - 1; + switch(bands) { + case 4: + while (in >= 0) { + a = pixel[in]; + if ((a <= 0) || (a >= 255)) { + in -= 4; + } else { + in--; + ialpha = fpNorm / a; + pixel[in] = (pixel[in] * ialpha + pt5) >>> 16; + in--; + pixel[in] = (pixel[in] * ialpha + pt5) >>> 16; + in--; + pixel[in] = (pixel[in] * ialpha + pt5) >>> 16; + in--; + } + } + break; + default: + while (in >= 0) { + a = pixel[in]; + if ((a <= 0) || (a >= 255)) { + in -= bands; + } else { + in--; + ialpha = fpNorm / a; + for (b = 0; b < bands - 1; b++) { + pixel[in] = (pixel[in] * ialpha + pt5) >>> 16; + in--; + } + } + } + } + dstR.setPixels(x0 + dx, y + dy, w, 1, pixel); + } + } else if (src.isAlphaPremultiplied()) { + int [] oPix = new int[bands * w]; + // Src has alpha dest does not so unpremult and store... + // System.out.println("Remove Alpha, Div Case"); + int a; + int b; + int ialpha; + int in; + int out; + int fpNorm = 0x00FF0000; + int pt5 = 1 << 15; + for (int y = y0; y <= y1; y++) { + pixel = srcR.getPixels(x0, y, w, 1, pixel); + in = (bands + 1) * w - 1; + out = (bands * w) - 1; + while (in >= 0) { + a = pixel[in]; + in--; + if (a > 0) { + if (a < 255) { + ialpha = fpNorm / a; + for (b = 0; b < bands; b++) { + oPix[out--] = (pixel[in--] * ialpha + pt5) >>> 16; + } + } else { + for (b = 0; b < bands; b++) { + oPix[out--] = pixel[in--]; + } + } + } else { + in -= bands; + for (b = 0; b < bands; b++) { + oPix[out--] = 255; + } + } + } + dstR.setPixels(x0 + dx, y + dy, w, 1, oPix); + } + } else { + // Src has unpremult alpha, dest does not have alpha, + // just copy the color channels over. + Rectangle dstRect = new Rectangle(destP.x, destP.y, + srcRect.width, srcRect.height); + for (int b = 0; b < bands; b++) { + copyBand(srcR, srcRect, b, + dstR, dstRect, b); + } + } + } + + public static void copyBand(Raster src, int srcBand, + WritableRaster dst, int dstBand) { + + Rectangle sR = src.getBounds(); + Rectangle dR = dst.getBounds(); + Rectangle cpR = sR.intersection(dR); + + copyBand(src, cpR, srcBand, dst, cpR, dstBand); + } + + public static void copyBand(Raster src, Rectangle sR, int sBand, + WritableRaster dst, Rectangle dR, int dBand) { + int dy = dR.y - sR.y; + int dx = dR.x - sR.x; + sR = sR.intersection(src.getBounds()); + dR = dR.intersection(dst.getBounds()); + int width; + int height; + if (dR.width < sR.width) { + width = dR.width; + } else { + width = sR.width; + } + if (dR.height < sR.height) { + height = dR.height; + } else { + height = sR.height; + } + int x = sR.x + dx; + int [] samples = null; + for (int y = sR.y; y < sR.y + height; y++) { + samples = src.getSamples(sR.x, y, width, 1, sBand, samples); + dst.setSamples(x, y + dy, width, 1, dBand, samples); + } + } + + public static boolean is_INT_PACK_Data(SampleModel sm, + boolean requireAlpha) { + // Check ColorModel is of type DirectColorModel + if (!(sm instanceof SinglePixelPackedSampleModel)) { + return false; + } + + // Check transfer type + if (sm.getDataType() != DataBuffer.TYPE_INT) { + return false; + } + + SinglePixelPackedSampleModel sppsm; + sppsm = (SinglePixelPackedSampleModel)sm; + + int [] masks = sppsm.getBitMasks(); + if (masks.length == 3) { + if (requireAlpha) { + return false; + } + } else if (masks.length != 4) { + return false; + } + + if (masks[0] != 0x00ff0000) { + return false; + } + if (masks[1] != 0x0000ff00) { + return false; + } + if (masks[2] != 0x000000ff) { + return false; + } + if ((masks.length == 4) + && (masks[3] != 0xff000000)) { + return false; + } + + return true; + } + + public static boolean is_BYTE_COMP_Data(SampleModel sm) { + // Check ColorModel is of type DirectColorModel + if (!(sm instanceof ComponentSampleModel)) { + return false; + } + + // Check transfer type + if (sm.getDataType() != DataBuffer.TYPE_BYTE) { + return false; + } + + return true; + } + + protected static void divide_INT_PACK_Data(WritableRaster wr) { + // System.out.println("Divide Int"); + + SinglePixelPackedSampleModel sppsm; + sppsm = (SinglePixelPackedSampleModel)wr.getSampleModel(); + + final int width = wr.getWidth(); + + final int scanStride = sppsm.getScanlineStride(); + DataBufferInt db = (DataBufferInt)wr.getDataBuffer(); + final int base + = (db.getOffset() + + sppsm.getOffset(wr.getMinX() - wr.getSampleModelTranslateX(), + wr.getMinY() - wr.getSampleModelTranslateY())); + + // Access the pixel data array + final int[] pixels = db.getBankData()[0]; + for (int y = 0; y < wr.getHeight(); y++) { + int sp = base + y * scanStride; + final int end = sp + width; + while (sp < end) { + int pixel = pixels[sp]; + int a = pixel >>> 24; + if (a <= 0) { + pixels[sp] = 0x00FFFFFF; + } else if (a < 255) { + int aFP = (0x00FF0000 / a); + pixels[sp] = + ((a << 24) + | (((((pixel & 0xFF0000) >> 16) * aFP) & 0xFF0000)) + | (((((pixel & 0x00FF00) >> 8) * aFP) & 0xFF0000) >> 8) + | (((((pixel & 0x0000FF)) * aFP) & 0xFF0000) >> 16)); + } + sp++; + } + } + } + + protected static void mult_INT_PACK_Data(WritableRaster wr) { + // System.out.println("Multiply Int: " + wr); + + SinglePixelPackedSampleModel sppsm; + sppsm = (SinglePixelPackedSampleModel)wr.getSampleModel(); + + final int width = wr.getWidth(); + + final int scanStride = sppsm.getScanlineStride(); + DataBufferInt db = (DataBufferInt)wr.getDataBuffer(); + final int base + = (db.getOffset() + + sppsm.getOffset(wr.getMinX() - wr.getSampleModelTranslateX(), + wr.getMinY() - wr.getSampleModelTranslateY())); + // Access the pixel data array + final int[] pixels = db.getBankData()[0]; + for (int y = 0; y < wr.getHeight(); y++) { + int sp = base + y * scanStride; + final int end = sp + width; + while (sp < end) { + int pixel = pixels[sp]; + int a = pixel >>> 24; + if ((a >= 0) && (a < 255)) { // this does NOT include a == 255 (0xff) ! + pixels[sp] = ((a << 24) + | ((((pixel & 0xFF0000) * a) >> 8) & 0xFF0000) + | ((((pixel & 0x00FF00) * a) >> 8) & 0x00FF00) + | ((((pixel & 0x0000FF) * a) >> 8) & 0x0000FF)); + } + sp++; + } + } + } + + + protected static void divide_BYTE_COMP_Data(WritableRaster wr) { + // System.out.println("Multiply Int: " + wr); + + ComponentSampleModel csm; + csm = (ComponentSampleModel)wr.getSampleModel(); + + final int width = wr.getWidth(); + + final int scanStride = csm.getScanlineStride(); + final int pixStride = csm.getPixelStride(); + final int [] bandOff = csm.getBandOffsets(); + + DataBufferByte db = (DataBufferByte)wr.getDataBuffer(); + final int base + = (db.getOffset() + + csm.getOffset(wr.getMinX() - wr.getSampleModelTranslateX(), + wr.getMinY() - wr.getSampleModelTranslateY())); + + int aOff = bandOff[bandOff.length - 1]; + int bands = bandOff.length - 1; + + // Access the pixel data array + final byte[] pixels = db.getBankData()[0]; + for (int y = 0; y < wr.getHeight(); y++) { + int sp = base + y * scanStride; + final int end = sp + width * pixStride; + while (sp < end) { + int a = pixels[sp + aOff] & 0xFF; + if (a == 0) { + for (int b = 0; b < bands; b++) { + pixels[sp + bandOff[b]] = (byte)0xFF; + } + } else if (a < 255) { // this does NOT include a == 255 (0xff) ! + int aFP = (0x00FF0000 / a); + for (int b = 0; b < bands; b++) { + int i = sp + bandOff[b]; + pixels[i] = (byte)(((pixels[i] & 0xFF) * aFP) >>> 16); + } + } + sp += pixStride; + } + } + } + + protected static void mult_BYTE_COMP_Data(WritableRaster wr) { + // System.out.println("Multiply Int: " + wr); + + ComponentSampleModel csm; + csm = (ComponentSampleModel)wr.getSampleModel(); + + final int width = wr.getWidth(); + + final int scanStride = csm.getScanlineStride(); + final int pixStride = csm.getPixelStride(); + final int [] bandOff = csm.getBandOffsets(); + + DataBufferByte db = (DataBufferByte)wr.getDataBuffer(); + final int base + = (db.getOffset() + + csm.getOffset(wr.getMinX() - wr.getSampleModelTranslateX(), + wr.getMinY() - wr.getSampleModelTranslateY())); + + + int aOff = bandOff[bandOff.length - 1]; + int bands = bandOff.length - 1; + + // Access the pixel data array + final byte[] pixels = db.getBankData()[0]; + for (int y = 0; y < wr.getHeight(); y++) { + int sp = base + y * scanStride; + final int end = sp + width * pixStride; + while (sp < end) { + int a = pixels[sp + aOff] & 0xFF; + if (a != 0xFF) { + for (int b = 0; b < bands; b++) { + int i = sp + bandOff[b]; + pixels[i] = (byte)(((pixels[i] & 0xFF) * a) >> 8); + } + } + sp += pixStride; + } + } + } + +/* + This is skanky debugging code that might be useful in the future: + + if (count == 33) { + String label = "sub [" + x + ", " + y + "]: "; + org.ImageDisplay.showImage + (label, subBI); + org.ImageDisplay.printImage + (label, subBI, + new Rectangle(75-iR.x, 90-iR.y, 32, 32)); + + } + + + // if ((count++ % 50) == 10) + // org.ImageDisplay.showImage("foo: ", subBI); + + + Graphics2D realG2D = g2d; + while (realG2D instanceof sun.java2d.ProxyGraphics2D) { + realG2D = ((sun.java2d.ProxyGraphics2D)realG2D).getDelegate(); + } + if (realG2D instanceof sun.awt.image.BufferedImageGraphics2D) { + count++; + if (count == 34) { + RenderedImage ri; + ri = ((sun.awt.image.BufferedImageGraphics2D)realG2D).bufImg; + // g2d.setComposite(SVGComposite.OVER); + // org.ImageDisplay.showImage("Bar: " + count, cr); + org.ImageDisplay.printImage("Bar: " + count, cr, + new Rectangle(75, 90, 32, 32)); + + org.ImageDisplay.showImage ("Foo: " + count, ri); + org.ImageDisplay.printImage("Foo: " + count, ri, + new Rectangle(75, 90, 32, 32)); + + System.out.println("BI: " + ri); + System.out.println("BISM: " + ri.getSampleModel()); + System.out.println("BICM: " + ri.getColorModel()); + System.out.println("BICM class: " + ri.getColorModel().getClass()); + System.out.println("BICS: " + ri.getColorModel().getColorSpace()); + System.out.println + ("sRGB CS: " + + ColorSpace.getInstance(ColorSpace.CS_sRGB)); + System.out.println("G2D info"); + System.out.println("\tComposite: " + g2d.getComposite()); + System.out.println("\tTransform" + g2d.getTransform()); + java.awt.RenderingHints rh = g2d.getRenderingHints(); + java.util.Set keys = rh.keySet(); + java.util.Iterator iter = keys.iterator(); + while (iter.hasNext()) { + Object o = iter.next(); + + System.out.println("\t" + o.toString() + " -> " + + rh.get(o).toString()); + } + + ri = cr; + System.out.println("RI: " + ri); + System.out.println("RISM: " + ri.getSampleModel()); + System.out.println("RICM: " + ri.getColorModel()); + System.out.println("RICM class: " + ri.getColorModel().getClass()); + System.out.println("RICS: " + ri.getColorModel().getColorSpace()); + } + } +*/ + + /** + * Extracts an alpha raster from a RenderedImage. The method tries to avoid copying data + * unnecessarily by checking if the RenderedImage is a BufferedImage which offers suitable + * direct methods. + * @param image the image + * @return the alpha raster + */ + public static Raster getAlphaRaster(RenderedImage image) { + ColorModel cm = image.getColorModel(); + if (!cm.hasAlpha() || cm.getTransparency() != ColorModel.TRANSLUCENT) { + throw new IllegalStateException("Image doesn't have an alpha channel"); + } + Raster alpha; + if (image instanceof BufferedImage) { + //Optimization possible with BufferedImage (No copying) + alpha = ((BufferedImage)image).getAlphaRaster(); + } else { + WritableRaster wraster = GraphicsUtil.makeRasterWritable(image.getData()); + alpha = image.getColorModel().getAlphaRaster(wraster); + } + return alpha; + } + + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/codec/png/PNGChunk.java b/src/main/java/org/apache/xmlgraphics/image/codec/png/PNGChunk.java new file mode 100644 index 0000000..68db864 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/codec/png/PNGChunk.java @@ -0,0 +1,184 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PNGChunk.java 1732019 2016-02-24 05:01:10Z gadams $ */ + +package org.apache.xmlgraphics.image.codec.png; + +import java.io.DataInputStream; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +public class PNGChunk { + int length; + int type; + byte[] data; + int crc; + + String typeString; + + /** logger */ + protected static final Log log = LogFactory.getLog(PNGChunk.class); + + /** + * See http://en.wikipedia.org/wiki/Portable_Network_Graphics for a light explanation; + * See http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html for the spec. + */ + public enum ChunkType { + IHDR, // IHDR must be the first chunk + PLTE, // PLTE contains the palette + IDAT, // IDAT contains the image, which may be split among multiple IDAT chunks + IEND, // IEND marks the image end + bKGD, // bKGD gives the default background color + cHRM, // cHRM gives the chromaticity coordinates + gAMA, // gAMA specifies gamma + hIST, // hIST can store the histogram + iCCP, // iCCP is an ICC color profile + iTXt, // iTXt contains UTF-8 text + pHYs, // pHYs holds the intended pixel size + sBIT, // sBIT (significant bits) indicates the color-accuracy + sPLT, // sPLT suggests a palette to use + sRGB, // sRGB indicates that the standard sRGB color space is used + sTER, // sTER stereo-image indicator chunk for stereoscopic images + tEXt, // tEXt can store text that can be represented in ISO/IEC 8859-1 + tIME, // tIME stores the time that the image was last changed + tRNS, // tRNS contains transparency information + zTXt; // zTXt contains compressed text with the same limits as tEXt + } + + public PNGChunk(int length, int type, byte[] data, int crc) { + this.length = length; + this.type = type; + this.data = data; + this.crc = crc; + this.typeString = typeIntToString(this.type); + } + + public int getLength() { + return length; + } + + public int getType() { + return type; + } + + public String getTypeString() { + return typeString; + } + + public byte[] getData() { + return data; + } + + public byte getByte(int offset) { + return data[offset]; + } + + public int getInt1(int offset) { + return data[offset] & 0xff; + } + + public int getInt2(int offset) { + return ((data[offset] & 0xff) << 8) | (data[offset + 1] & 0xff); + } + + public int getInt4(int offset) { + return ((data[offset] & 0xff) << 24) | ((data[offset + 1] & 0xff) << 16) + | ((data[offset + 2] & 0xff) << 8) | (data[offset + 3] & 0xff); + } + + public String getString4(int offset) { + return "" + (char) data[offset] + (char) data[offset + 1] + (char) data[offset + 2] + + (char) data[offset + 3]; + } + + public boolean isType(String typeName) { + return typeString.equals(typeName); + } + + /** + * Reads the next chunk from the input stream. + * @param distream the input stream + * @return the chunk + */ + public static PNGChunk readChunk(DataInputStream distream) { + try { + int length = distream.readInt(); + int type = distream.readInt(); + byte[] data = new byte[length]; + distream.readFully(data); + int crc = distream.readInt(); + + return new PNGChunk(length, type, data, crc); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + /** + * Returns the PNG chunk type, a four letter case sensitive ASCII type/name. + * @param distream the input stream + * @return a four letter case sensitive ASCII type/name + */ + public static String getChunkType(DataInputStream distream) { + try { + distream.mark(8); + /* int length = */distream.readInt(); + int type = distream.readInt(); + distream.reset(); + + return typeIntToString(type); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + private static String typeIntToString(int type) { + String typeString = ""; + typeString += (char) (type >> 24); + typeString += (char) ((type >> 16) & 0xff); + typeString += (char) ((type >> 8) & 0xff); + typeString += (char) (type & 0xff); + return typeString; + } + + /** + * Skips the next chunk from the input stream. + * @param distream the input stream + * @return true if skipping successful, false otherwise + */ + public static boolean skipChunk(DataInputStream distream) { + try { + int length = distream.readInt(); + distream.readInt(); + // is this really faster than reading? + int skipped = distream.skipBytes(length); + distream.readInt(); + if (skipped != length) { + log.warn("Incorrect number of bytes skipped."); + return false; + } + return true; + } catch (Exception e) { + log.warn(e.getMessage()); + return false; + } + } +} diff --git a/src/main/java/org/apache/xmlgraphics/image/codec/png/PNGDecodeParam.java b/src/main/java/org/apache/xmlgraphics/image/codec/png/PNGDecodeParam.java new file mode 100644 index 0000000..ba0fa7b --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/codec/png/PNGDecodeParam.java @@ -0,0 +1,357 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PNGDecodeParam.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.image.codec.png; + +import org.apache.xmlgraphics.image.codec.util.ImageDecodeParam; +import org.apache.xmlgraphics.image.codec.util.PropertyUtil; + +// CSOFF: WhitespaceAround + +/** + * An instance of ImageDecodeParam for decoding images in + * the PNG format. + * + * PNGDecodeParam allows several aspects of the decoding + * process for PNG images to be controlled. By default, decoding produces + * output images with the following properties: + * + *

    Images with a bit depth of 8 or less use a + * DataBufferByte to hold the pixel data. 16-bit images + * use a DataBufferUShort. + * + *

    Palette color images and non-transparent grayscale images with + * bit depths of 1, 2, or 4 will have a + * MultiPixelPackedSampleModel and an + * IndexColorModel. For palette color images, the + * ColorModel palette contains the red, green, blue, and + * optionally alpha palette information. For grayscale images, the + * palette is used to expand the pixel data to cover the range 0-255. + * The pixels are stored packed 8, 4, or 2 to the byte. + * + *

    All other images are stored using a + * PixelInterleavedSampleModel with each sample of a pixel + * occupying its own byte or short within + * the DataBuffer. A ComponentColorModel is + * used which simply extracts the red, green, blue, gray, and/or alpha + * information from separate DataBuffer entries. + * + *

    Five aspects of this process may be altered by means of methods + * in this class. + * + *

    setSuppressAlpha() prevents an alpha channel + * from appearing in the output. + * + *

    setExpandPalette() turns palette-color images into + * 3-or 4-channel full-color images. + * + *

    setOutput8BitGray() causes 1, 2, or 4 bit + * grayscale images to be output in 8-bit form, using a + * ComponentSampleModel and + * ComponentColorModel. + * + *

    setDecodingExponent() causes the output image to be + * gamma-corrected using a supplied output gamma value. + * + *

    setExpandGrayAlpha() causes 2-channel gray/alpha + * (GA) images to be output as full-color (GGGA) images, which may + * simplify further processing and display. + * + *

    This class is not a committed part of the JAI API. It may + * be removed or changed in future releases of JAI. + */ +public class PNGDecodeParam implements ImageDecodeParam { + + private static final long serialVersionUID = 3957265194926624623L; + + /** + * Constructs a default instance of PNGDecodeParam. + */ + public PNGDecodeParam() { } + + private boolean suppressAlpha; + + /** + * Returns true if alpha (transparency) will + * be prevented from appearing in the output. + */ + public boolean getSuppressAlpha() { + return suppressAlpha; + } + + /** + * If set, no alpha (transparency) channel will appear in the + * output image. + * + *

    The default is to allow transparency to appear in the + * output image. + */ + public void setSuppressAlpha(boolean suppressAlpha) { + this.suppressAlpha = suppressAlpha; + } + + private boolean expandPalette; + + /** + * Returns true if palette-color images will be expanded to + * produce full-color output. + */ + public boolean getExpandPalette() { + return expandPalette; + } + + /** + * If set, palette color images (PNG color type 3) will + * be decoded into full-color (RGB) output images. The output + * image may have 3 or 4 channels, depending on the presence of + * transparency information. + * + *

    The default is to output palette images using a single + * channel. The palette information is used to construct the + * output image's ColorModel. + */ + public void setExpandPalette(boolean expandPalette) { + this.expandPalette = expandPalette; + } + + private boolean output8BitGray; + + /** + * Returns the current value of the 8-bit gray output parameter. + */ + public boolean getOutput8BitGray() { + return output8BitGray; + } + + /** + * If set, grayscale images with a bit depth less than 8 + * (1, 2, or 4) will be output in 8 bit form. The output values + * will occupy the full 8-bit range. For example, gray values + * 0, 1, 2, and 3 of a 2-bit image will be output as + * 0, 85, 170, and 255. + * + *

    The decoding of non-grayscale images and grayscale images + * with a bit depth of 8 or 16 are unaffected by this setting. + * + *

    The default is not to perform expansion. Grayscale images + * with a depth of 1, 2, or 4 bits will be represented using + * a MultiPixelPackedSampleModel and an + * IndexColorModel. + */ + public void setOutput8BitGray(boolean output8BitGray) { + this.output8BitGray = output8BitGray; + } + + private boolean performGammaCorrection = true; + + /** + * Returns true if gamma correction is to be performed + * on the image data. The default is true. + * + *

    If gamma correction is to be performed, the + * getUserExponent() and + * getDisplayExponent() methods are used in addition to + * the gamma value stored within the file (or the default value of + * 1/2.2 used if no value is found) to produce a single exponent + * using the formula: + *

    +     * decoding_exponent = user_exponent/(gamma_from_file * display_exponent)
    +     * 
    + */ + public boolean getPerformGammaCorrection() { + return performGammaCorrection; + } + + /** + * Turns gamma corection of the image data on or off. + */ + public void setPerformGammaCorrection(boolean performGammaCorrection) { + this.performGammaCorrection = performGammaCorrection; + } + + private float userExponent = 1.0F; + + /** + * Returns the current value of the user exponent parameter. + * By default, the user exponent is equal to 1.0F. + */ + public float getUserExponent() { + return userExponent; + } + + /** + * Sets the user exponent to a given value. The exponent + * must be positive. If not, an + * IllegalArgumentException will be thrown. + * + *

    The output image pixels will be placed through a transformation + * of the form: + * + *

    +     * sample = integer_sample / (2^bitdepth - 1.0)
    +     * decoding_exponent = user_exponent/(gamma_from_file * display_exponent)
    +     * output = sample ^ decoding_exponent
    +     * 
    + * + * where gamma_from_file is the gamma of the file + * data, as determined by the gAMA, sRGB, + * and/or iCCP chunks, and display_exponent + * is the exponent of the intrinsic transfer curve of the display, + * generally 2.2. + * + *

    Input files which do not specify any gamma are assumed to + * have a gamma of 1/2.2; such images may be displayed + * on a CRT with an exponent of 2.2 using the default user + * exponent of 1.0. + * + *

    The user exponent may be used in order to change the + * effective gamma of a file. If a file has a stored gamma of + * X, but the decoder believes that the true file gamma is Y, + * setting a user exponent of Y/X will produce the same result + * as changing the file gamma. + * + *

    This parameter affects the decoding of all image types. + * + * @throws IllegalArgumentException if userExponent is + * negative. + */ + public void setUserExponent(float userExponent) { + if (userExponent <= 0.0F) { + throw new IllegalArgumentException(PropertyUtil.getString("PNGDecodeParam0")); + } + this.userExponent = userExponent; + } + + private float displayExponent = 2.2F; + + /** + * Returns the current value of the display exponent parameter. + * By default, the display exponent is equal to 2.2F. + */ + public float getDisplayExponent() { + return displayExponent; + } + + /** + * Sets the display exponent to a given value. The exponent + * must be positive. If not, an + * IllegalArgumentException will be thrown. + * + *

    The output image pixels will be placed through a transformation + * of the form: + * + *

    +     * sample = integer_sample / (2^bitdepth - 1.0)
    +     * decoding_exponent = user_exponent/(gamma_from_file * display_exponent)
    +     * output = sample ^ decoding_exponent
    +     * 
    + * + * where gamma_from_file is the gamma of the file + * data, as determined by the gAMA, sRGB, + * and/or iCCP chunks, and user_exponent + * is an additional user-supplied parameter. + * + *

    Input files which do not specify any gamma are assumed to + * have a gamma of 1/2.2; such images should be + * decoding using the default display exponent of 2.2. + * + *

    If an image is to be processed further before being displayed, + * it may be preferable to set the display exponent to 1.0 in order + * to produce a linear output image. + * + *

    This parameter affects the decoding of all image types. + * + * @throws IllegalArgumentException if userExponent is + * negative. + */ + public void setDisplayExponent(float displayExponent) { + if (displayExponent <= 0.0F) { + throw new IllegalArgumentException(PropertyUtil.getString("PNGDecodeParam1")); + } + this.displayExponent = displayExponent; + } + + private boolean expandGrayAlpha; + + /** + * Returns the current setting of the gray/alpha expansion. + */ + public boolean getExpandGrayAlpha() { + return expandGrayAlpha; + } + + /** + * If set, images containing one channel of gray and one channel of + * alpha (GA) will be output in a 4-channel format (GGGA). This + * produces output that may be simpler to process and display. + * + *

    This setting affects both images of color type 4 (explicit + * alpha) and images of color type 0 (grayscale) that contain + * transparency information. + * + *

    By default, no expansion is performed. + */ + public void setExpandGrayAlpha(boolean expandGrayAlpha) { + this.expandGrayAlpha = expandGrayAlpha; + } + + private boolean generateEncodeParam; + + private PNGEncodeParam encodeParam; + + /** + * Returns true if an instance of + * PNGEncodeParam will be available after an image + * has been decoded via the getEncodeParam method. + */ + public boolean getGenerateEncodeParam() { + return generateEncodeParam; + } + + /** + * If set, an instance of PNGEncodeParam will be + * available after an image has been decoded via the + * getEncodeParam method that encapsulates information + * about the contents of the PNG file. If not set, this information + * will not be recorded and getEncodeParam() will + * return null. + */ + public void setGenerateEncodeParam(boolean generateEncodeParam) { + this.generateEncodeParam = generateEncodeParam; + } + + /** + * If getGenerateEncodeParam() is true, + * this method may be called after decoding has completed, and + * will return an instance of PNGEncodeParam containing + * information about the contents of the PNG file just decoded. + */ + public PNGEncodeParam getEncodeParam() { + return encodeParam; + } + + /** + * Sets the current encoder param instance. This method is + * intended to be called by the PNG decoder and will overwrite the + * current instance returned by getEncodeParam. + */ + public void setEncodeParam(PNGEncodeParam encodeParam) { + this.encodeParam = encodeParam; + } +} diff --git a/src/main/java/org/apache/xmlgraphics/image/codec/png/PNGEncodeParam.java b/src/main/java/org/apache/xmlgraphics/image/codec/png/PNGEncodeParam.java new file mode 100644 index 0000000..12daea4 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/codec/png/PNGEncodeParam.java @@ -0,0 +1,1531 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PNGEncodeParam.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.image.codec.png; + +import java.awt.image.ColorModel; +import java.awt.image.IndexColorModel; +import java.awt.image.RenderedImage; +import java.awt.image.SampleModel; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import org.apache.xmlgraphics.image.codec.util.ImageEncodeParam; +import org.apache.xmlgraphics.image.codec.util.PropertyUtil; + +// CSOFF: MemberName +// CSOFF: MultipleVariableDeclarations +// CSOFF: NeedBraces +// CSOFF: OperatorWrap +// CSOFF: ParameterName +// CSOFF: WhitespaceAround + +/** + * An instance of ImageEncodeParam for encoding images in + * the PNG format. + * + *

    This class is not a committed part of the JAI API. It may + * be removed or changed in future releases of JAI. + */ +public abstract class PNGEncodeParam implements ImageEncodeParam { + private static final long serialVersionUID = -7851509538552141263L; + + /** Constant for use with the sRGB chunk. */ + public static final int INTENT_PERCEPTUAL = 0; + + /** Constant for use with the sRGB chunk. */ + public static final int INTENT_RELATIVE = 1; + + /** Constant for use with the sRGB chunk. */ + public static final int INTENT_SATURATION = 2; + + /** Constant for use with the sRGB chunk. */ + public static final int INTENT_ABSOLUTE = 3; + + /** Constant for use in filtering. */ + public static final int PNG_FILTER_NONE = 0; + + /** Constant for use in filtering. */ + public static final int PNG_FILTER_SUB = 1; + + /** Constant for use in filtering. */ + public static final int PNG_FILTER_UP = 2; + + /** Constant for use in filtering. */ + public static final int PNG_FILTER_AVERAGE = 3; + + /** Constant for use in filtering. */ + public static final int PNG_FILTER_PAETH = 4; + + + /** + * Returns an instance of PNGEncodeParam.Palette, + * PNGEncodeParam.Gray, or + * PNGEncodeParam.RGB appropriate for encoding + * the given image. + * + *

    If the image has an IndexColorModel, an + * instance of PNGEncodeParam.Palette is returned. + * Otherwise, if the image has 1 or 2 bands an instance of + * PNGEncodeParam.Gray is returned. In all other + * cases an instance of PNGEncodeParam.RGB is + * returned. + * + *

    Note that this method does not provide any guarantee that + * the given image will be successfully encoded by the PNG + * encoder, as it only performs a very superficial analysis of + * the image structure. + */ + public static PNGEncodeParam getDefaultEncodeParam(RenderedImage im) { + ColorModel colorModel = im.getColorModel(); + if (colorModel instanceof IndexColorModel) { + return new PNGEncodeParam.Palette(); + } + + SampleModel sampleModel = im.getSampleModel(); + int numBands = sampleModel.getNumBands(); + + if (numBands == 1 || numBands == 2) { + return new PNGEncodeParam.Gray(); + } else { + return new PNGEncodeParam.RGB(); + } + } + + public static class Palette extends PNGEncodeParam { + + private static final long serialVersionUID = -5181545170427733891L; + + /** Constructs an instance of PNGEncodeParam.Palette. */ + public Palette() { } + + // bKGD chunk + + private boolean backgroundSet; + + /** + * Suppresses the 'bKGD' chunk from being output. + */ + public void unsetBackground() { + backgroundSet = false; + } + + /** + * Returns true if a 'bKGD' chunk will be output. + */ + public boolean isBackgroundSet() { + return backgroundSet; + } + + /** + * Sets the desired bit depth for a palette image. The bit + * depth must be one of 1, 2, 4, or 8, or else an + * IllegalArgumentException will be thrown. + */ + public void setBitDepth(int bitDepth) { + if (bitDepth != 1 && bitDepth != 2 && bitDepth != 4 + && bitDepth != 8) { + throw new IllegalArgumentException(PropertyUtil.getString("PNGEncodeParam2")); + } + this.bitDepth = bitDepth; + bitDepthSet = true; + } + + // PLTE chunk + + private int[] palette; + private boolean paletteSet; + + /** + * Sets the RGB palette of the image to be encoded. + * The rgb parameter contains alternating + * R, G, B values for each color index used in the image. + * The number of elements must be a multiple of 3 between + * 3 and 3*256. + * + *

    The 'PLTE' chunk will encode this information. + * + * @param rgb An array of ints. + */ + public void setPalette(int[] rgb) { + if (rgb.length < 1 * 3 || rgb.length > 256 * 3) { + throw new + IllegalArgumentException(PropertyUtil.getString("PNGEncodeParam0")); + } + if ((rgb.length % 3) != 0) { + throw new + IllegalArgumentException(PropertyUtil.getString("PNGEncodeParam1")); + } + + palette = (int[])(rgb.clone()); + paletteSet = true; + } + + /** + * Returns the current RGB palette. + * + *

    If the palette has not previously been set, or has been + * unset, an IllegalStateException will be thrown. + * + * @throws IllegalStateException if the palette is not set. + * + * @return An array of ints. + */ + public int[] getPalette() { + if (!paletteSet) { + throw new IllegalStateException(PropertyUtil.getString("PNGEncodeParam3")); + } + return (int[])(palette.clone()); + } + + /** + * Suppresses the 'PLTE' chunk from being output. + */ + public void unsetPalette() { + palette = null; + paletteSet = false; + } + + /** + * Returns true if a 'PLTE' chunk will be output. + */ + public boolean isPaletteSet() { + return paletteSet; + } + + // bKGD chunk + + private int backgroundPaletteIndex; + + /** + * Sets the palette index of the suggested background color. + * + *

    The 'bKGD' chunk will encode this information. + */ + public void setBackgroundPaletteIndex(int index) { + backgroundPaletteIndex = index; + backgroundSet = true; + } + + /** + * Returns the palette index of the suggested background color. + * + *

    If the background palette index has not previously been + * set, or has been unset, an + * IllegalStateException will be thrown. + * + * @throws IllegalStateException if the palette index is not set. + */ + public int getBackgroundPaletteIndex() { + if (!backgroundSet) { + throw new IllegalStateException(PropertyUtil.getString("PNGEncodeParam4")); + } + return backgroundPaletteIndex; + } + + // tRNS chunk + + private int[] transparency; + + /** + * Sets the alpha values associated with each palette entry. + * The alpha parameter should have as many entries + * as there are RGB triples in the palette. + * + *

    The 'tRNS' chunk will encode this information. + */ + public void setPaletteTransparency(byte[] alpha) { + transparency = new int[alpha.length]; + for (int i = 0; i < alpha.length; i++) { + transparency[i] = alpha[i] & 0xff; + } + transparencySet = true; + } + + /** + * Returns the alpha values associated with each palette entry. + * + *

    If the palette transparency has not previously been + * set, or has been unset, an + * IllegalStateException will be thrown. + * + * @throws IllegalStateException if the palette transparency is + * not set. + */ + public byte[] getPaletteTransparency() { + if (!transparencySet) { + throw new IllegalStateException(PropertyUtil.getString("PNGEncodeParam5")); + } + byte[] alpha = new byte[transparency.length]; + for (int i = 0; i < alpha.length; i++) { + alpha[i] = (byte)transparency[i]; + } + return alpha; + } + } + + public static class Gray extends PNGEncodeParam { + + private static final long serialVersionUID = -2055439792025795274L; + + /** Constructs an instance of PNGEncodeParam.Gray. */ + public Gray() { } + + // bKGD chunk + + private boolean backgroundSet; + + /** + * Suppresses the 'bKGD' chunk from being output. + */ + public void unsetBackground() { + backgroundSet = false; + } + + /** + * Returns true if a 'bKGD' chunk will be output. + */ + public boolean isBackgroundSet() { + return backgroundSet; + } + + /** + * Sets the desired bit depth for a grayscale image. The bit + * depth must be one of 1, 2, 4, 8, or 16. + * + *

    When encoding a source image of a greater bit depth, + * pixel values will be clamped to the smaller range after + * shifting by the value given by getBitShift(). + * When encoding a source image of a smaller bit depth, pixel + * values will be shifted and left-filled with zeroes. + */ + public void setBitDepth(int bitDepth) { + if (bitDepth != 1 && bitDepth != 2 && bitDepth != 4 + && bitDepth != 8 && bitDepth != 16) { + throw new IllegalArgumentException(PropertyUtil.getString("PNGEncodeParam2")); + } + this.bitDepth = bitDepth; + bitDepthSet = true; + } + + // bKGD chunk + + private int backgroundPaletteGray; + + /** + * Sets the suggested gray level of the background. + * + *

    The 'bKGD' chunk will encode this information. + */ + public void setBackgroundGray(int gray) { + backgroundPaletteGray = gray; + backgroundSet = true; + } + + /** + * Returns the suggested gray level of the background. + * + *

    If the background gray level has not previously been + * set, or has been unset, an + * IllegalStateException will be thrown. + * + * @throws IllegalStateException if the background gray level + * is not set. + */ + public int getBackgroundGray() { + if (!backgroundSet) { + throw new IllegalStateException(PropertyUtil.getString("PNGEncodeParam6")); + } + return backgroundPaletteGray; + } + + // tRNS chunk + + private int[] transparency; + + /** + * Sets the gray value to be used to denote transparency. + * + *

    Setting this attribute will cause the alpha channel + * of the input image to be ignored. + * + *

    The 'tRNS' chunk will encode this information. + */ + public void setTransparentGray(int transparentGray) { + transparency = new int[1]; + transparency[0] = transparentGray; + transparencySet = true; + } + + /** + * Returns the gray value to be used to denote transparency. + * + *

    If the transparent gray value has not previously been + * set, or has been unset, an + * IllegalStateException will be thrown. + * + * @throws IllegalStateException if the transparent gray value + * is not set. + */ + public int getTransparentGray() { + if (!transparencySet) { + throw new IllegalStateException(PropertyUtil.getString("PNGEncodeParam7")); + } + int gray = transparency[0]; + return gray; + } + + private int bitShift; + private boolean bitShiftSet; + + /** + * Sets the desired bit shift for a grayscale image. + * Pixels in the source image will be shifted right by + * the given amount prior to being clamped to the maximum + * value given by the encoded image's bit depth. + */ + public void setBitShift(int bitShift) { + if (bitShift < 0) { + throw new IllegalArgumentException(PropertyUtil.getString("PNGEncodeParam25")); + } + this.bitShift = bitShift; + bitShiftSet = true; + } + + /** + * Returns the desired bit shift for a grayscale image. + * + *

    If the bit shift has not previously been set, or has been + * unset, an IllegalStateException will be thrown. + * + * @throws IllegalStateException if the bit shift is not set. + */ + public int getBitShift() { + if (!bitShiftSet) { + throw new IllegalStateException(PropertyUtil.getString("PNGEncodeParam8")); + } + return bitShift; + } + + /** + * Suppresses the setting of the bit shift of a grayscale image. + * Pixels in the source image will not be shifted prior to encoding. + */ + public void unsetBitShift() { + bitShiftSet = false; + } + + /** + * Returns true if the bit shift has been set. + */ + public boolean isBitShiftSet() { + return bitShiftSet; + } + + /** + * Returns true if the bit depth has been set. + */ + public boolean isBitDepthSet() { + return bitDepthSet; + } + } + + public static class RGB extends PNGEncodeParam { + + private static final long serialVersionUID = -8918762026006670891L; + + /** Constructs an instance of PNGEncodeParam.RGB. */ + public RGB() { } + + // bKGD chunk + + private boolean backgroundSet; + + /** + * Suppresses the 'bKGD' chunk from being output. + */ + public void unsetBackground() { + backgroundSet = false; + } + + /** + * Returns true if a 'bKGD' chunk will be output. + */ + public boolean isBackgroundSet() { + return backgroundSet; + } + + /** + * Sets the desired bit depth for an RGB image. The bit + * depth must be 8 or 16. + */ + public void setBitDepth(int bitDepth) { + if (bitDepth != 8 && bitDepth != 16) { + throw new IllegalArgumentException(PropertyUtil.getString("PNGEncodeParam26")); + } + this.bitDepth = bitDepth; + bitDepthSet = true; + } + + // bKGD chunk + + private int[] backgroundRGB; + + /** + * Sets the RGB value of the suggested background color. + * The rgb parameter should have 3 entries. + * + *

    The 'bKGD' chunk will encode this information. + */ + public void setBackgroundRGB(int[] rgb) { + if (rgb.length != 3) { + throw new IllegalArgumentException(PropertyUtil.getString("PNGEncodeParam27")); + } + backgroundRGB = rgb; + backgroundSet = true; + } + + /** + * Returns the RGB value of the suggested background color. + * + *

    If the background color has not previously been set, or has been + * unset, an IllegalStateException will be thrown. + * + * @throws IllegalStateException if the background color is not set. + */ + public int[] getBackgroundRGB() { + if (!backgroundSet) { + throw new IllegalStateException(PropertyUtil.getString("PNGEncodeParam9")); + } + return backgroundRGB; + } + + // tRNS chunk + + private int[] transparency; + + /** + * Sets the RGB value to be used to denote transparency. + * + *

    Setting this attribute will cause the alpha channel + * of the input image to be ignored. + * + *

    The 'tRNS' chunk will encode this information. + */ + public void setTransparentRGB(int[] transparentRGB) { + transparency = (int[])(transparentRGB.clone()); + transparencySet = true; + } + + /** + * Returns the RGB value to be used to denote transparency. + * + *

    If the transparent color has not previously been set, + * or has been unset, an IllegalStateException + * will be thrown. + * + * @throws IllegalStateException if the transparent color is not set. + */ + public int[] getTransparentRGB() { + if (!transparencySet) { + throw new IllegalStateException(PropertyUtil.getString("PNGEncodeParam10")); + } + return (int[])(transparency.clone()); + } + } + + protected int bitDepth; + protected boolean bitDepthSet; + + /** + * Sets the desired bit depth of an image. + */ + public abstract void setBitDepth(int bitDepth); + + /** + * Returns the desired bit depth for a grayscale image. + * + *

    If the bit depth has not previously been set, or has been + * unset, an IllegalStateException will be thrown. + * + * @throws IllegalStateException if the bit depth is not set. + */ + public int getBitDepth() { + if (!bitDepthSet) { + throw new IllegalStateException(PropertyUtil.getString("PNGEncodeParam11")); + } + return bitDepth; + } + + /** + * Suppresses the setting of the bit depth of a grayscale image. + * The depth of the encoded image will be inferred from the source + * image bit depth, rounded up to the next power of 2 between 1 + * and 16. + */ + public void unsetBitDepth() { + bitDepthSet = false; + } + + private boolean useInterlacing; + + /** + * Turns Adam7 interlacing on or off. + */ + public void setInterlacing(boolean useInterlacing) { + this.useInterlacing = useInterlacing; + } + + /** + * Returns true if Adam7 interlacing will be used. + */ + public boolean getInterlacing() { + return useInterlacing; + } + + // bKGD chunk - delegate to subclasses + + // In JAI 1.0, 'backgroundSet' was private. The JDK 1.2 compiler + // was lenient and incorrectly allowed this variable to be + // accessed from the subclasses. The JDK 1.3 compiler correctly + // flags this as a use of a non-static variable in a static + // context. Changing 'backgroundSet' to protected would have + // solved the problem, but would have introduced a visible API + // change. Thus we are forced to adopt the solution of placing a + // separate private variable in each subclass and providing + // separate implementations of 'unsetBackground' and + // 'isBackgroundSet' in each concrete subclass. + + /** + * Suppresses the 'bKGD' chunk from being output. + * For API compatibility with JAI 1.0, the superclass + * defines this method to throw a RuntimeException; + * accordingly, subclasses must provide their own implementations. + */ + public void unsetBackground() { + throw new RuntimeException(PropertyUtil.getString("PNGEncodeParam23")); + } + + /** + * Returns true if a 'bKGD' chunk will be output. + * For API compatibility with JAI 1.0, the superclass + * defines this method to throw a RuntimeException; + * accordingly, subclasses must provide their own implementations. + */ + public boolean isBackgroundSet() { + throw new RuntimeException(PropertyUtil.getString("PNGEncodeParam24")); + } + + // cHRM chunk + + private float[] chromaticity; + private boolean chromaticitySet; + + /** + * Sets the white point and primary chromaticities in CIE (x, y) + * space. + * + *

    The chromaticity parameter should be a + * float array of length 8 containing the white point + * X and Y, red X and Y, green X and Y, and blue X and Y values in + * order. + * + *

    The 'cHRM' chunk will encode this information. + */ + public void setChromaticity(float[] chromaticity) { + if (chromaticity.length != 8) { + throw new IllegalArgumentException(PropertyUtil.getString("PNGEncodeParam28")); + } + this.chromaticity = (float[])(chromaticity.clone()); + chromaticitySet = true; + } + + /** + * A convenience method that calls the array version. + */ + public void setChromaticity(float whitePointX, float whitePointY, + float redX, float redY, + float greenX, float greenY, + float blueX, float blueY) { + float[] chroma = new float[8]; + chroma[0] = whitePointX; + chroma[1] = whitePointY; + chroma[2] = redX; + chroma[3] = redY; + chroma[4] = greenX; + chroma[5] = greenY; + chroma[6] = blueX; + chroma[7] = blueY; + setChromaticity(chroma); + } + + /** + * Returns the white point and primary chromaticities in + * CIE (x, y) space. + * + *

    See the documentation for the setChromaticity + * method for the format of the returned data. + * + *

    If the chromaticity has not previously been set, or has been + * unset, an IllegalStateException will be thrown. + * + * @throws IllegalStateException if the chromaticity is not set. + */ + public float[] getChromaticity() { + if (!chromaticitySet) { + throw new IllegalStateException(PropertyUtil.getString("PNGEncodeParam12")); + } + return (float[])(chromaticity.clone()); + } + + /** + * Suppresses the 'cHRM' chunk from being output. + */ + public void unsetChromaticity() { + chromaticity = null; + chromaticitySet = false; + } + + /** + * Returns true if a 'cHRM' chunk will be output. + */ + public boolean isChromaticitySet() { + return chromaticitySet; + } + + // gAMA chunk + + private float gamma; + private boolean gammaSet; + + /** + * Sets the file gamma value for the image. + * + *

    The 'gAMA' chunk will encode this information. + */ + public void setGamma(float gamma) { + this.gamma = gamma; + gammaSet = true; + } + + /** + * Returns the file gamma value for the image. + * + *

    If the file gamma has not previously been set, or has been + * unset, an IllegalStateException will be thrown. + * + * @throws IllegalStateException if the gamma is not set. + */ + public float getGamma() { + if (!gammaSet) { + throw new IllegalStateException(PropertyUtil.getString("PNGEncodeParam13")); + } + return gamma; + } + + /** + * Suppresses the 'gAMA' chunk from being output. + */ + public void unsetGamma() { + gammaSet = false; + } + + /** + * Returns true if a 'gAMA' chunk will be output. + */ + public boolean isGammaSet() { + return gammaSet; + } + + // hIST chunk + + private int[] paletteHistogram; + private boolean paletteHistogramSet; + + /** + * Sets the palette histogram to be stored with this image. + * The histogram consists of an array of integers, one per + * palette entry. + * + *

    The 'hIST' chunk will encode this information. + */ + public void setPaletteHistogram(int[] paletteHistogram) { + this.paletteHistogram = (int[])(paletteHistogram.clone()); + paletteHistogramSet = true; + } + + /** + * Returns the palette histogram to be stored with this image. + * + *

    If the histogram has not previously been set, or has been + * unset, an IllegalStateException will be thrown. + * + * @throws IllegalStateException if the histogram is not set. + */ + public int[] getPaletteHistogram() { + if (!paletteHistogramSet) { + throw new IllegalStateException(PropertyUtil.getString("PNGEncodeParam14")); + } + return paletteHistogram; + } + + /** + * Suppresses the 'hIST' chunk from being output. + */ + public void unsetPaletteHistogram() { + paletteHistogram = null; + paletteHistogramSet = false; + } + + /** + * Returns true if a 'hIST' chunk will be output. + */ + public boolean isPaletteHistogramSet() { + return paletteHistogramSet; + } + + // iCCP chunk + + private byte[] iccProfileData; + private boolean iccProfileDataSet; + + /** + * Sets the ICC profile data to be stored with this image. + * The profile is represented in raw binary form. + * + *

    The 'iCCP' chunk will encode this information. + */ + public void setICCProfileData(byte[] iccProfileData) { + this.iccProfileData = (byte[])(iccProfileData.clone()); + iccProfileDataSet = true; + } + + /** + * Returns the ICC profile data to be stored with this image. + * + *

    If the ICC profile has not previously been set, or has been + * unset, an IllegalStateException will be thrown. + * + * @throws IllegalStateException if the ICC profile is not set. + */ + public byte[] getICCProfileData() { + if (!iccProfileDataSet) { + throw new IllegalStateException(PropertyUtil.getString("PNGEncodeParam15")); + } + return (byte[])(iccProfileData.clone()); + } + + /** + * Suppresses the 'iCCP' chunk from being output. + */ + public void unsetICCProfileData() { + iccProfileData = null; + iccProfileDataSet = false; + } + + /** + * Returns true if a 'iCCP' chunk will be output. + */ + public boolean isICCProfileDataSet() { + return iccProfileDataSet; + } + + // pHYS chunk + + private int[] physicalDimension; + private boolean physicalDimensionSet; + + /** + * Sets the physical dimension information to be stored with this + * image. The physicalDimension parameter should be a 3-entry + * array containing the number of pixels per unit in the X + * direction, the number of pixels per unit in the Y direction, + * and the unit specifier (0 = unknown, 1 = meters). + * + *

    The 'pHYS' chunk will encode this information. + */ + public void setPhysicalDimension(int[] physicalDimension) { + this.physicalDimension = (int[])(physicalDimension.clone()); + physicalDimensionSet = true; + } + + /** + * A convenience method that calls the array version. + */ + public void setPhysicalDimension(int xPixelsPerUnit, + int yPixelsPerUnit, + int unitSpecifier) { + int[] pd = new int[3]; + pd[0] = xPixelsPerUnit; + pd[1] = yPixelsPerUnit; + pd[2] = unitSpecifier; + + setPhysicalDimension(pd); + } + + /** + * Returns the physical dimension information to be stored + * with this image. + * + *

    If the physical dimension information has not previously + * been set, or has been unset, an + * IllegalStateException will be thrown. + * + * @throws IllegalStateException if the physical dimension information + * is not set. + */ + public int[] getPhysicalDimension() { + if (!physicalDimensionSet) { + throw new IllegalStateException(PropertyUtil.getString("PNGEncodeParam16")); + } + return (int[])(physicalDimension.clone()); + } + + /** + * Suppresses the 'pHYS' chunk from being output. + */ + public void unsetPhysicalDimension() { + physicalDimension = null; + physicalDimensionSet = false; + } + + /** + * Returns true if a 'pHYS' chunk will be output. + */ + public boolean isPhysicalDimensionSet() { + return physicalDimensionSet; + } + + // sPLT chunk + + private PNGSuggestedPaletteEntry[] suggestedPalette; + private boolean suggestedPaletteSet; + + /** + * Sets the suggested palette information to be stored with this + * image. The information is passed to this method as an array of + * PNGSuggestedPaletteEntry objects. + * + *

    The 'sPLT' chunk will encode this information. + */ + public void setSuggestedPalette(PNGSuggestedPaletteEntry[] palette) { + suggestedPalette = (PNGSuggestedPaletteEntry[])(palette.clone()); + suggestedPaletteSet = true; + } + + /** + * Returns the suggested palette information to be stored with this + * image. + * + *

    If the suggested palette information has not previously + * been set, or has been unset, an + * IllegalStateException will be thrown. + * + * @throws IllegalStateException if the suggested palette + * information is not set. + */ + public PNGSuggestedPaletteEntry[] getSuggestedPalette() { + if (!suggestedPaletteSet) { + throw new IllegalStateException(PropertyUtil.getString("PNGEncodeParam17")); + } + return (PNGSuggestedPaletteEntry[])(suggestedPalette.clone()); + } + + /** + * Suppresses the 'sPLT' chunk from being output. + */ + public void unsetSuggestedPalette() { + suggestedPalette = null; + suggestedPaletteSet = false; + } + + /** + * Returns true if a 'sPLT' chunk will be output. + */ + public boolean isSuggestedPaletteSet() { + return suggestedPaletteSet; + } + + // sBIT chunk + + private int[] significantBits; + private boolean significantBitsSet; + + /** + * Sets the number of significant bits for each band of the image. + * + *

    The number of entries in the significantBits + * array must be equal to the number of output bands in the image: + * 1 for a gray image, 2 for gray+alpha, 3 for index or truecolor, + * and 4 for truecolor+alpha. + * + *

    The 'sBIT' chunk will encode this information. + */ + public void setSignificantBits(int[] significantBits) { + this.significantBits = (int[])(significantBits.clone()); + significantBitsSet = true; + } + + /** + * Returns the number of significant bits for each band of the image. + * + *

    If the significant bits values have not previously been + * set, or have been unset, an IllegalStateException + * will be thrown. + * + * @throws IllegalStateException if the significant bits values are + * not set. + */ + public int[] getSignificantBits() { + if (!significantBitsSet) { + throw new IllegalStateException(PropertyUtil.getString("PNGEncodeParam18")); + } + return (int[])significantBits.clone(); + } + + /** + * Suppresses the 'sBIT' chunk from being output. + */ + public void unsetSignificantBits() { + significantBits = null; + significantBitsSet = false; + } + + /** + * Returns true if an 'sBIT' chunk will be output. + */ + public boolean isSignificantBitsSet() { + return significantBitsSet; + } + + // sRGB chunk + + private int srgbIntent; + private boolean srgbIntentSet; + + /** + * Sets the sRGB rendering intent to be stored with this image. + * The legal values are 0 = Perceptual, 1 = Relative Colorimetric, + * 2 = Saturation, and 3 = Absolute Colorimetric. Refer to the + * PNG specification for information on these values. + * + *

    The 'sRGB' chunk will encode this information. + */ + public void setSRGBIntent(int srgbIntent) { + this.srgbIntent = srgbIntent; + srgbIntentSet = true; + } + + /** + * Returns the sRGB rendering intent to be stored with this image. + * + *

    If the sRGB intent has not previously been set, or has been + * unset, an IllegalStateException will be thrown. + * + * @throws IllegalStateException if the sRGB intent is not set. + */ + public int getSRGBIntent() { + if (!srgbIntentSet) { + throw new IllegalStateException(PropertyUtil.getString("PNGEncodeParam19")); + } + return srgbIntent; + } + + /** + * Suppresses the 'sRGB' chunk from being output. + */ + public void unsetSRGBIntent() { + srgbIntentSet = false; + } + + /** + * Returns true if an 'sRGB' chunk will be output. + */ + public boolean isSRGBIntentSet() { + return srgbIntentSet; + } + + // tEXt chunk + + private String[] text; + private boolean textSet; + + /** + * Sets the textual data to be stored in uncompressed form with this + * image. The data is passed to this method as an array of + * Strings. + * + *

    The 'tEXt' chunk will encode this information. + */ + public void setText(String[] text) { + this.text = text; + textSet = true; + } + + /** + * Returns the text strings to be stored in uncompressed form with this + * image as an array of Strings. + * + *

    If the text strings have not previously been set, or have been + * unset, an IllegalStateException will be thrown. + * + * @throws IllegalStateException if the text strings are not set. + */ + public String[] getText() { + if (!textSet) { + throw new IllegalStateException(PropertyUtil.getString("PNGEncodeParam20")); + } + return text; + } + + /** + * Suppresses the 'tEXt' chunk from being output. + */ + public void unsetText() { + text = null; + textSet = false; + } + + /** + * Returns true if a 'tEXt' chunk will be output. + */ + public boolean isTextSet() { + return textSet; + } + + // tIME chunk + + private Date modificationTime; + private boolean modificationTimeSet; + + /** + * Sets the modification time, as a Date, to be + * stored with this image. The internal storage format will use + * UTC regardless of how the modificationTime + * parameter was created. + * + *

    The 'tIME' chunk will encode this information. + */ + public void setModificationTime(Date modificationTime) { + this.modificationTime = modificationTime; + modificationTimeSet = true; + } + + /** + * Returns the modification time to be stored with this image. + * + *

    If the bit depth has not previously been set, or has been + * unset, an IllegalStateException will be thrown. + * + * @throws IllegalStateException if the bit depth is not set. + */ + public Date getModificationTime() { + if (!modificationTimeSet) { + throw new IllegalStateException(PropertyUtil.getString("PNGEncodeParam21")); + } + return modificationTime; + } + + /** + * Suppresses the 'tIME' chunk from being output. + */ + public void unsetModificationTime() { + modificationTime = null; + modificationTimeSet = false; + } + + /** + * Returns true if a 'tIME' chunk will be output. + */ + public boolean isModificationTimeSet() { + return modificationTimeSet; + } + + // tRNS chunk + + boolean transparencySet; + + /** + * Suppresses the 'tRNS' chunk from being output. + */ + public void unsetTransparency() { + transparencySet = false; + } + + /** + * Returns true if a 'tRNS' chunk will be output. + */ + public boolean isTransparencySet() { + return transparencySet; + } + + // zTXT chunk + + private String[] zText; + private boolean zTextSet; + + /** + * Sets the text strings to be stored in compressed form with this + * image. The data is passed to this method as an array of + * Strings. + * + *

    The 'zTXt' chunk will encode this information. + */ + public void setCompressedText(String[] text) { + this.zText = text; + zTextSet = true; + } + + /** + * Returns the text strings to be stored in compressed form with + * this image as an array of Strings. + * + *

    If the compressed text strings have not previously been + * set, or have been unset, an IllegalStateException + * will be thrown. + * + * @throws IllegalStateException if the compressed text strings are + * not set. + */ + public String[] getCompressedText() { + if (!zTextSet) { + throw new IllegalStateException(PropertyUtil.getString("PNGEncodeParam22")); + } + return zText; + } + + /** + * Suppresses the 'zTXt' chunk from being output. + */ + public void unsetCompressedText() { + zText = null; + zTextSet = false; + } + + /** + * Returns true if a 'zTXT' chunk will be output. + */ + public boolean isCompressedTextSet() { + return zTextSet; + } + + // Other chunk types + + List chunkType = new ArrayList(); + List chunkData = new ArrayList(); + + /** + * Adds a private chunk, in binary form, to the list of chunks to + * be stored with this image. + * + * @param type a 4-character String giving the chunk type name. + * @param data an array of bytes containing the + * chunk data. + */ + public synchronized void addPrivateChunk(String type, byte[] data) { + chunkType.add(type); + chunkData.add(data.clone()); + } + + /** + * Returns the number of private chunks to be written to the + * output file. + */ + public synchronized int getNumPrivateChunks() { + return chunkType.size(); + } + + /** + * Returns the type of the private chunk at a given index, as a + * 4-character String. The index must be smaller + * than the return value of getNumPrivateChunks. + */ + public synchronized String getPrivateChunkType(int index) { + return (String)chunkType.get(index); + } + + /** + * Returns the data associated of the private chunk at a given + * index, as an array of bytes. The index must be + * smaller than the return value of + * getNumPrivateChunks. + */ + public synchronized byte[] getPrivateChunkData(int index) { + return (byte[])chunkData.get(index); + } + + /** + * Remove all private chunks associated with this parameter instance + * whose 'safe-to-copy' bit is not set. This may be advisable when + * transcoding PNG images. + */ + public synchronized void removeUnsafeToCopyPrivateChunks() { + List newChunkType = new ArrayList(); + List newChunkData = new ArrayList(); + + int len = getNumPrivateChunks(); + for (int i = 0; i < len; i++) { + String type = getPrivateChunkType(i); + char lastChar = type.charAt(3); + if (lastChar >= 'a' && lastChar <= 'z') { + newChunkType.add(type); + newChunkData.add(getPrivateChunkData(i)); + } + } + + chunkType = newChunkType; + chunkData = newChunkData; + } + + /** + * Remove all private chunks associated with this parameter instance. + */ + public synchronized void removeAllPrivateChunks() { + chunkType = new ArrayList(); + chunkData = new ArrayList(); + } + + /** + * An abs() function for use by the Paeth predictor. + */ + private static int abs(int x) { + return (x < 0) ? -x : x; + } + + /** + * The Paeth predictor routine used in PNG encoding. This routine + * is included as a convenience to subclasses that override the + * filterRow method. + */ + public static int paethPredictor(int a, int b, int c) { + int p = a + b - c; + int pa = abs(p - a); + int pb = abs(p - b); + int pc = abs(p - c); + + if ((pa <= pb) && (pa <= pc)) { + return a; + } else if (pb <= pc) { + return b; + } else { + return c; + } + } + + /** + * Performs filtering on a row of an image. This method may be + * overridden in order to provide a custom algorithm for choosing + * the filter type for a given row. + * + *

    The method is supplied with the current and previous rows + * of the image. For the first row of the image, or of an + * interlacing pass, the previous row array will be filled with + * zeros as required by the PNG specification. + * + *

    The method is also supplied with five scratch arrays. + * These arrays may be used within the method for any purpose. + * At method exit, the array at the index given by the return + * value of the method should contain the filtered data. The + * return value will also be used as the filter type. + * + *

    The default implementation of the method performs a trial + * encoding with each of the filter types, and computes the sum of + * absolute values of the differences between the raw bytes of the + * current row and the predicted values. The index of the filter + * producing the smallest result is returned. + * + *

    As an example, to perform only 'sub' filtering, this method + * could be implemented (non-optimally) as follows: + * + *

    +     * for (int i = bytesPerPixel; i < bytesPerRow + bytesPerPixel; i++) {
    +     *     int curr = currRow[i] & 0xff;
    +     *     int left = currRow[i - bytesPerPixel] & 0xff;
    +     *     scratchRow[PNG_FILTER_SUB][i] = (byte)(curr - left);
    +     * }
    +     * return PNG_FILTER_SUB;
    +     * 
    + * + * @param currRow The current row as an array of bytes + * of length at least bytesPerRow + bytesPerPixel. + * The pixel data starts at index bytesPerPixel; + * the initial bytesPerPixel bytes are zero. + * @param prevRow The current row as an array of bytes + * The pixel data starts at index bytesPerPixel; + * the initial bytesPerPixel bytes are zero. + * @param scratchRows An array of 5 byte arrays of + * length at least bytesPerRow + + * bytesPerPixel, useable to hold temporary results. + * The filtered row will be returned as one of the entries + * of this array. The returned filtered data should start + * at index bytesPerPixel; The initial + * bytesPerPixel bytes are not used. + * @param bytesPerRow The number of bytes in the image row. + * This value will always be greater than 0. + * @param bytesPerPixel The number of bytes representing a single + * pixel, rounded up to an integer. This is the 'bpp' parameter + * described in the PNG specification. + * + * @return The filter type to be used. The entry of + * scratchRows[] at this index holds the + * filtered data. */ + public int filterRow(byte[] currRow, + byte[] prevRow, + byte[][] scratchRows, + int bytesPerRow, + int bytesPerPixel) { + + int [] badness = {0, 0, 0, 0, 0}; + int curr; + int left; + int up; + int upleft; + int diff; + int pa; + int pb; + int pc; + for (int i = bytesPerPixel; i < bytesPerRow + bytesPerPixel; i++) { + curr = currRow[i] & 0xff; + left = currRow[i - bytesPerPixel] & 0xff; + up = prevRow[i] & 0xff; + upleft = prevRow[i - bytesPerPixel] & 0xff; + + // no filter + badness[0] += curr; + + // sub filter + diff = curr - left; + scratchRows[1][i] = (byte)diff; + badness [1] += (diff > 0) ? diff : -diff; + + // up filter + diff = curr - up; + scratchRows[2][i] = (byte)diff; + badness [2] += (diff >= 0) ? diff : -diff; + + // average filter + diff = curr - ((left + up) >> 1); + scratchRows[3][i] = (byte)diff; + badness [3] += (diff >= 0) ? diff : -diff; + + // paeth filter + + // Original code much simplier but doesn't take full + // advantage of relationship between pa/b/c and + // information gleaned in abs operations. + /// pa = up -upleft; + /// pb = left-upleft; + /// pc = pa+pb; + /// pa = abs(pa); + /// pb = abs(pb); + /// pc = abs(pc); + /// if ((pa <= pb) && (pa <= pc)) + /// diff = curr-left; + /// else if (pb <= pc) + /// diff = curr-up; + /// else + /// diff = curr-upleft; + + pa = up - upleft; + pb = left - upleft; + if (pa < 0) { + if (pb < 0) { + // both pa & pb neg so pc is always greater than or + // equal to pa or pb; + if (pa >= pb) { // since pa & pb neg check sense is reversed. + diff = curr - left; + } else { + diff = curr - up; + } + } else { + // pa neg pb pos so we must compute pc... + pc = pa + pb; + pa = -pa; + if (pa <= pb) { // pc is positive and less than pb + if (pa <= pc) { + diff = curr - left; + } else { + diff = curr - upleft; + } + } else { + // pc is negative and less than or equal to pa, + // but since pa is greater than pb this isn't an issue... + if (pb <= -pc) { + diff = curr - up; + } else { + diff = curr - upleft; + } + } + } + } else { + if (pb < 0) { + pb = -pb; // make it positive... + if (pa <= pb) { + // pc would be negative and less than or equal to pb + pc = pb - pa; + if (pa <= pc) { + diff = curr - left; + } else if (pb == pc) { + // if pa is zero then pc==pb otherwise + // pc must be less than pb. + diff = curr - up; + } else { + diff = curr - upleft; + } + } else { + // pc would be positive and less than pa. + pc = pa - pb; + if (pb <= pc) { + diff = curr - up; + } else { + diff = curr - upleft; + } + } + } else { + // both pos so pa+pb is always greater than pa/pb + if (pa <= pb) { + diff = curr - left; + } else { + diff = curr - up; + } + } + } + scratchRows[4][i] = (byte)diff; + badness [4] += (diff >= 0) ? diff : -diff; + } + int filterType = 0; + int minBadness = badness[0]; + + for (int i = 1; i < 5; i++) { + if (badness[i] < minBadness) { + minBadness = badness[i]; + filterType = i; + } + } + + if (filterType == 0) { + System.arraycopy(currRow, bytesPerPixel, + scratchRows[0], bytesPerPixel, + bytesPerRow); + } + + return filterType; + } +} diff --git a/src/main/java/org/apache/xmlgraphics/image/codec/png/PNGImageDecoder.java b/src/main/java/org/apache/xmlgraphics/image/codec/png/PNGImageDecoder.java new file mode 100644 index 0000000..5ab3edd --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/codec/png/PNGImageDecoder.java @@ -0,0 +1,1799 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PNGImageDecoder.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.image.codec.png; + +import java.awt.Color; +import java.awt.Point; +import java.awt.Transparency; +import java.awt.color.ColorSpace; +import java.awt.image.ColorModel; +import java.awt.image.ComponentColorModel; +import java.awt.image.DataBuffer; +import java.awt.image.DataBufferByte; +import java.awt.image.DataBufferUShort; +import java.awt.image.IndexColorModel; +import java.awt.image.Raster; +import java.awt.image.RenderedImage; +import java.awt.image.SampleModel; +import java.awt.image.WritableRaster; +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.SequenceInputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.Locale; +import java.util.TimeZone; +import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; + +import javax.imageio.stream.ImageInputStream; + +import org.apache.xmlgraphics.image.codec.util.ImageDecoderImpl; +import org.apache.xmlgraphics.image.codec.util.ImageInputStreamSeekableStreamAdapter; +import org.apache.xmlgraphics.image.codec.util.PropertyUtil; +import org.apache.xmlgraphics.image.codec.util.SeekableStream; +import org.apache.xmlgraphics.image.codec.util.SimpleRenderedImage; +import org.apache.xmlgraphics.image.loader.ImageSize; +import org.apache.xmlgraphics.image.loader.impl.PNGConstants; + +// CSOFF: ConstantName +// CSOFF: InnerAssignment +// CSOFF: MethodName +// CSOFF: MissingSwitchDefault +// CSOFF: MultipleVariableDeclarations +// CSOFF: NoWhitespaceAfter +// CSOFF: OperatorWrap +// CSOFF: WhitespaceAround + +/** + * @version $Id: PNGImageDecoder.java 1732018 2016-02-24 04:51:06Z gadams $ + */ +public class PNGImageDecoder extends ImageDecoderImpl { + + public PNGImageDecoder(InputStream input, + PNGDecodeParam param) { + super(input, param); + } + + @Override + public RenderedImage decodeAsRenderedImage(int page) throws IOException { + if (page != 0) { + throw new IOException(PropertyUtil.getString("PNGImageDecoder19")); + } + return new PNGImage(input, (PNGDecodeParam)param); + } + + public static void readPNGHeader(ImageInputStream inputStream, ImageSize size) throws IOException { + SeekableStream seekStream = new ImageInputStreamSeekableStreamAdapter(inputStream) { + public void close() throws IOException { + } + }; + PNGImage pngImage = new PNGImage(seekStream); + size.setSizeInPixels(pngImage.getWidth(), pngImage.getHeight()); + double dpiHorz = size.getDpiHorizontal(); + double dpiVert = size.getDpiVertical(); + if (pngImage.unitSpecifier == 1) { + if (pngImage.xPixelsPerUnit != 0) { + dpiHorz = pngImage.xPixelsPerUnit * 0.0254; + } + if (pngImage.yPixelsPerUnit != 0) { + dpiVert = pngImage.yPixelsPerUnit * 0.0254; + } + } + size.setResolution(dpiHorz, dpiVert); + size.calcSizeFromPixels(); + } +} + +/** + * TO DO: + * + * zTXt chunks + * + */ +class PNGImage extends SimpleRenderedImage implements PNGConstants { + + private static final String[] colorTypeNames = { + "Grayscale", "Error", "Truecolor", "Index", + "Grayscale with alpha", "Error", "Truecolor with alpha" + }; + + private int[][] bandOffsets = { + null, + { 0 }, // G + { 0, 1 }, // GA in GA order + { 0, 1, 2 }, // RGB in RGB order + { 0, 1, 2, 3 } // RGBA in RGBA order + }; + + private int bitDepth; + private int colorType; + + private int compressionMethod; + private int filterMethod; + private int interlaceMethod; + + private int paletteEntries; + private byte[] redPalette; + private byte[] greenPalette; + private byte[] bluePalette; + private byte[] alphaPalette; + + private int bkgdRed; + private int bkgdGreen; + private int bkgdBlue; + + private int grayTransparentAlpha; + private int redTransparentAlpha; + private int greenTransparentAlpha; + private int blueTransparentAlpha; + + private int maxOpacity; + + private int[] significantBits; + + // Parameter information + + // If true, the user wants destination alpha where applicable. + private boolean suppressAlpha; + + // If true, perform palette lookup internally + private boolean expandPalette; + + // If true, output < 8 bit gray images in 8 bit components format + private boolean output8BitGray; + + // Create an alpha channel in the destination color model. + private boolean outputHasAlphaPalette; + + // Perform gamma correction on the image + private boolean performGammaCorrection; + + // Expand GA to GGGA for compatbility with Java2D + private boolean expandGrayAlpha; + + // Produce an instance of PNGEncodeParam + private boolean generateEncodeParam; + + // PNGDecodeParam controlling decode process + private PNGDecodeParam decodeParam; + + // PNGEncodeParam to store file details in + private PNGEncodeParam encodeParam; + + private boolean emitProperties = true; + + private float fileGamma = 45455 / 100000.0F; + + private float userExponent = 1.0F; + + private float displayExponent = 2.2F; + + private float[] chromaticity; + + private int sRGBRenderingIntent = -1; + + // Post-processing step implied by above parameters + private int postProcess = POST_NONE; + + protected int xPixelsPerUnit; + protected int yPixelsPerUnit; + protected int unitSpecifier; + + // Possible post-processing steps + + // Do nothing + private static final int POST_NONE = 0; + + // Gamma correct only + private static final int POST_GAMMA = 1; + + // Push gray values through grayLut to expand to 8 bits + private static final int POST_GRAY_LUT = 2; + + // Push gray values through grayLut to expand to 8 bits, add alpha + private static final int POST_GRAY_LUT_ADD_TRANS = 3; + + // Push palette value through R,G,B lookup tables + private static final int POST_PALETTE_TO_RGB = 4; + + // Push palette value through R,G,B,A lookup tables + private static final int POST_PALETTE_TO_RGBA = 5; + + // Add transparency to a given gray value (w/ optional gamma) + private static final int POST_ADD_GRAY_TRANS = 6; + + // Add transparency to a given RGB value (w/ optional gamma) + private static final int POST_ADD_RGB_TRANS = 7; + + // Remove the alpha channel from a gray image (w/ optional gamma) + private static final int POST_REMOVE_GRAY_TRANS = 8; + + // Remove the alpha channel from an RGB image (w/optional gamma) + private static final int POST_REMOVE_RGB_TRANS = 9; + + // Mask to add expansion of GA -> GGGA + private static final int POST_EXP_MASK = 16; + + // Expand gray to G/G/G + private static final int POST_GRAY_ALPHA_EXP = + POST_NONE | POST_EXP_MASK; + + // Expand gray to G/G/G through a gamma lut + private static final int POST_GAMMA_EXP = + POST_GAMMA | POST_EXP_MASK; + + // Push gray values through grayLut to expand to 8 bits, expand, add alpha + private static final int POST_GRAY_LUT_ADD_TRANS_EXP = + POST_GRAY_LUT_ADD_TRANS | POST_EXP_MASK; + + // Add transparency to a given gray value, expand + private static final int POST_ADD_GRAY_TRANS_EXP = + POST_ADD_GRAY_TRANS | POST_EXP_MASK; + + private List streamVec = new ArrayList(); + private DataInputStream dataStream; + + private int bytesPerPixel; // number of bytes per input pixel + private int inputBands; + private int outputBands; + + // Number of private chunks + private int chunkIndex; + + private List textKeys = new ArrayList(); + private List textStrings = new ArrayList(); + + private List ztextKeys = new ArrayList(); + private List ztextStrings = new ArrayList(); + + private WritableRaster theTile; + + private int[] gammaLut; + + private void initGammaLut(int bits) { + double exp = (double)userExponent / (fileGamma * displayExponent); + int numSamples = 1 << bits; + int maxOutSample = (bits == 16) ? 65535 : 255; + + gammaLut = new int[numSamples]; + for (int i = 0; i < numSamples; i++) { + double gbright = (double)i / (numSamples - 1); + double gamma = Math.pow(gbright, exp); + int igamma = (int)(gamma * maxOutSample + 0.5); + if (igamma > maxOutSample) { + igamma = maxOutSample; + } + gammaLut[i] = igamma; + } + } + + private final byte[][] expandBits = { + null, + { (byte)0x00, (byte)0xff }, + { (byte)0x00, (byte)0x55, (byte)0xaa, (byte)0xff }, + null, + { (byte)0x00, (byte)0x11, (byte)0x22, (byte)0x33, + (byte)0x44, (byte)0x55, (byte)0x66, (byte)0x77, + (byte)0x88, (byte)0x99, (byte)0xaa, (byte)0xbb, + (byte)0xcc, (byte)0xdd, (byte)0xee, (byte)0xff } + }; + + private int[] grayLut; + + private void initGrayLut(int bits) { + int len = 1 << bits; + grayLut = new int[len]; + + if (performGammaCorrection) { + System.arraycopy(gammaLut, 0, grayLut, 0, len); + } else { + for (int i = 0; i < len; i++) { + grayLut[i] = expandBits[bits][i]; + } + } + } + + public PNGImage(InputStream stream) throws IOException { + DataInputStream distream = new DataInputStream(stream); + long magic = distream.readLong(); + if (magic != PNG_SIGNATURE) { + throw new IOException("Not a png file"); + } + while (true) { + String chunkType = PNGChunk.getChunkType(distream); + if (chunkType.equals(PNGChunk.ChunkType.IHDR.name())) { + PNGChunk chunk = PNGChunk.readChunk(distream); + parse_IHDR_chunk(chunk); + } else if (chunkType.equals(PNGChunk.ChunkType.pHYs.name())) { + PNGChunk chunk = PNGChunk.readChunk(distream); + parse_pHYs_chunk(chunk); + return; + } else if (chunkType.equals(PNGChunk.ChunkType.IEND.name())) { + return; + } else { + PNGChunk.readChunk(distream); + } + } + } + + public PNGImage(InputStream stream, PNGDecodeParam decodeParam) + throws IOException { + + if (!stream.markSupported()) { + stream = new BufferedInputStream(stream); + } + DataInputStream distream = new DataInputStream(stream); + + if (decodeParam == null) { + decodeParam = new PNGDecodeParam(); + } + this.decodeParam = decodeParam; + + // Get parameter values + this.suppressAlpha = decodeParam.getSuppressAlpha(); + this.expandPalette = decodeParam.getExpandPalette(); + this.output8BitGray = decodeParam.getOutput8BitGray(); + this.expandGrayAlpha = decodeParam.getExpandGrayAlpha(); + if (decodeParam.getPerformGammaCorrection()) { + this.userExponent = decodeParam.getUserExponent(); + this.displayExponent = decodeParam.getDisplayExponent(); + performGammaCorrection = true; + output8BitGray = true; + } + this.generateEncodeParam = decodeParam.getGenerateEncodeParam(); + + if (emitProperties) { + properties.put("file_type", "PNG v. 1.0"); + } + + try { + long magic = distream.readLong(); + if (magic != PNG_SIGNATURE) { + String msg = PropertyUtil.getString("PNGImageDecoder0"); + throw new RuntimeException(msg); + } + } catch (IOException ioe) { + ioe.printStackTrace(); + String msg = PropertyUtil.getString("PNGImageDecoder1"); + throw new RuntimeException(msg); + } + + do { +// try { + PNGChunk chunk; + + String chunkType = PNGChunk.getChunkType(distream); + if (chunkType.equals(PNGChunk.ChunkType.IHDR.name())) { + chunk = PNGChunk.readChunk(distream); + parse_IHDR_chunk(chunk); + } else if (chunkType.equals(PNGChunk.ChunkType.PLTE.name())) { + chunk = PNGChunk.readChunk(distream); + parse_PLTE_chunk(chunk); + } else if (chunkType.equals(PNGChunk.ChunkType.IDAT.name())) { + chunk = PNGChunk.readChunk(distream); + streamVec.add(new ByteArrayInputStream(chunk.getData())); + } else if (chunkType.equals(PNGChunk.ChunkType.IEND.name())) { + chunk = PNGChunk.readChunk(distream); + try { + parse_IEND_chunk(chunk); + } catch (Exception e) { + e.printStackTrace(); + String msg = PropertyUtil.getString("PNGImageDecoder2"); + throw new RuntimeException(msg); + } + break; // fall through to the bottom + } else if (chunkType.equals(PNGChunk.ChunkType.bKGD.name())) { + chunk = PNGChunk.readChunk(distream); + parse_bKGD_chunk(chunk); + } else if (chunkType.equals(PNGChunk.ChunkType.cHRM.name())) { + chunk = PNGChunk.readChunk(distream); + parse_cHRM_chunk(chunk); + } else if (chunkType.equals(PNGChunk.ChunkType.gAMA.name())) { + chunk = PNGChunk.readChunk(distream); + parse_gAMA_chunk(chunk); + } else if (chunkType.equals(PNGChunk.ChunkType.hIST.name())) { + chunk = PNGChunk.readChunk(distream); + parse_hIST_chunk(chunk); + } else if (chunkType.equals(PNGChunk.ChunkType.iCCP.name())) { + chunk = PNGChunk.readChunk(distream); + parse_iCCP_chunk(chunk); + } else if (chunkType.equals(PNGChunk.ChunkType.pHYs.name())) { + chunk = PNGChunk.readChunk(distream); + parse_pHYs_chunk(chunk); + } else if (chunkType.equals(PNGChunk.ChunkType.sBIT.name())) { + chunk = PNGChunk.readChunk(distream); + parse_sBIT_chunk(chunk); + } else if (chunkType.equals(PNGChunk.ChunkType.sRGB.name())) { + chunk = PNGChunk.readChunk(distream); + parse_sRGB_chunk(chunk); + } else if (chunkType.equals(PNGChunk.ChunkType.tEXt.name())) { + chunk = PNGChunk.readChunk(distream); + parse_tEXt_chunk(chunk); + } else if (chunkType.equals(PNGChunk.ChunkType.tIME.name())) { + chunk = PNGChunk.readChunk(distream); + parse_tIME_chunk(chunk); + } else if (chunkType.equals(PNGChunk.ChunkType.tRNS.name())) { + chunk = PNGChunk.readChunk(distream); + parse_tRNS_chunk(chunk); + } else if (chunkType.equals(PNGChunk.ChunkType.zTXt.name())) { + chunk = PNGChunk.readChunk(distream); + parse_zTXt_chunk(chunk); + } else { + chunk = PNGChunk.readChunk(distream); + // Output the chunk data in raw form + + String type = chunk.getTypeString(); + byte[] data = chunk.getData(); + if (encodeParam != null) { + encodeParam.addPrivateChunk(type, data); + } + if (emitProperties) { + String key = "chunk_" + chunkIndex++ + ':' + type; + properties.put(key.toLowerCase(Locale.getDefault()), data); + } + } +// } catch (Exception e) { +// e.printStackTrace(); +// String msg = PropertyUtil.getString("PNGImageDecoder2"); +// throw new RuntimeException(msg); +// } + } while (true); + + // Final post-processing + + if (significantBits == null) { + significantBits = new int[inputBands]; + for (int i = 0; i < inputBands; i++) { + significantBits[i] = bitDepth; + } + + if (emitProperties) { + properties.put("significant_bits", significantBits); + } + } + } + + private void parse_IHDR_chunk(PNGChunk chunk) { + tileWidth = width = chunk.getInt4(0); + tileHeight = height = chunk.getInt4(4); + + bitDepth = chunk.getInt1(8); + + if ((bitDepth != 1) && (bitDepth != 2) && (bitDepth != 4) + && (bitDepth != 8) && (bitDepth != 16)) { + // Error -- bad bit depth + String msg = PropertyUtil.getString("PNGImageDecoder3"); + throw new RuntimeException(msg); + } + maxOpacity = (1 << bitDepth) - 1; + + colorType = chunk.getInt1(9); + if ((colorType != PNG_COLOR_GRAY) + && (colorType != PNG_COLOR_RGB) + && (colorType != PNG_COLOR_PALETTE) + && (colorType != PNG_COLOR_GRAY_ALPHA) + && (colorType != PNG_COLOR_RGB_ALPHA)) { + System.out.println(PropertyUtil.getString("PNGImageDecoder4")); + } + + if ((colorType == PNG_COLOR_RGB) && (bitDepth < 8)) { + // Error -- RGB images must have 8 or 16 bits + String msg = PropertyUtil.getString("PNGImageDecoder5"); + throw new RuntimeException(msg); + } + + if ((colorType == PNG_COLOR_PALETTE) && (bitDepth == 16)) { + // Error -- palette images must have < 16 bits + String msg = PropertyUtil.getString("PNGImageDecoder6"); + throw new RuntimeException(msg); + } + + if ((colorType == PNG_COLOR_GRAY_ALPHA) && (bitDepth < 8)) { + // Error -- gray/alpha images must have >= 8 bits + String msg = PropertyUtil.getString("PNGImageDecoder7"); + throw new RuntimeException(msg); + } + + if ((colorType == PNG_COLOR_RGB_ALPHA) && (bitDepth < 8)) { + // Error -- RGB/alpha images must have >= 8 bits + String msg = PropertyUtil.getString("PNGImageDecoder8"); + throw new RuntimeException(msg); + } + + if (emitProperties) { + properties.put("color_type", colorTypeNames[colorType]); + } + + if (generateEncodeParam) { + if (colorType == PNG_COLOR_PALETTE) { + encodeParam = new PNGEncodeParam.Palette(); + } else if (colorType == PNG_COLOR_GRAY + || colorType == PNG_COLOR_GRAY_ALPHA) { + encodeParam = new PNGEncodeParam.Gray(); + } else { + encodeParam = new PNGEncodeParam.RGB(); + } + decodeParam.setEncodeParam(encodeParam); + } + + if (encodeParam != null) { + encodeParam.setBitDepth(bitDepth); + } + if (emitProperties) { + properties.put("bit_depth", bitDepth); + } + + if (performGammaCorrection) { + // Assume file gamma is 1/2.2 unless we get a gAMA chunk + float gamma = (1.0F / 2.2F) * (displayExponent / userExponent); + if (encodeParam != null) { + encodeParam.setGamma(gamma); + } + if (emitProperties) { + properties.put("gamma", gamma); + } + } + + compressionMethod = chunk.getInt1(10); + if (compressionMethod != 0) { + // Error -- only know about compression method 0 + String msg = PropertyUtil.getString("PNGImageDecoder9"); + throw new RuntimeException(msg); + } + + filterMethod = chunk.getInt1(11); + if (filterMethod != 0) { + // Error -- only know about filter method 0 + String msg = PropertyUtil.getString("PNGImageDecoder10"); + throw new RuntimeException(msg); + } + + interlaceMethod = chunk.getInt1(12); + if (interlaceMethod == 0) { + if (encodeParam != null) { + encodeParam.setInterlacing(false); + } + if (emitProperties) { + properties.put("interlace_method", "None"); + } + } else if (interlaceMethod == 1) { + if (encodeParam != null) { + encodeParam.setInterlacing(true); + } + if (emitProperties) { + properties.put("interlace_method", "Adam7"); + } + } else { + // Error -- only know about Adam7 interlacing + String msg = PropertyUtil.getString("PNGImageDecoder11"); + throw new RuntimeException(msg); + } + + bytesPerPixel = (bitDepth == 16) ? 2 : 1; + + switch (colorType) { + case PNG_COLOR_GRAY: + inputBands = 1; + outputBands = 1; + + if (output8BitGray && (bitDepth < 8)) { + postProcess = POST_GRAY_LUT; + } else if (performGammaCorrection) { + postProcess = POST_GAMMA; + } else { + postProcess = POST_NONE; + } + break; + + case PNG_COLOR_RGB: + inputBands = 3; + bytesPerPixel *= 3; + outputBands = 3; + + if (performGammaCorrection) { + postProcess = POST_GAMMA; + } else { + postProcess = POST_NONE; + } + break; + + case PNG_COLOR_PALETTE: + inputBands = 1; + bytesPerPixel = 1; + outputBands = expandPalette ? 3 : 1; + + if (expandPalette) { + postProcess = POST_PALETTE_TO_RGB; + } else { + postProcess = POST_NONE; + } + break; + + case PNG_COLOR_GRAY_ALPHA: + inputBands = 2; + bytesPerPixel *= 2; + + if (suppressAlpha) { + outputBands = 1; + postProcess = POST_REMOVE_GRAY_TRANS; + } else { + if (performGammaCorrection) { + postProcess = POST_GAMMA; + } else { + postProcess = POST_NONE; + } + if (expandGrayAlpha) { + postProcess |= POST_EXP_MASK; + outputBands = 4; + } else { + outputBands = 2; + } + } + break; + + case PNG_COLOR_RGB_ALPHA: + inputBands = 4; + bytesPerPixel *= 4; + outputBands = (!suppressAlpha) ? 4 : 3; + + if (suppressAlpha) { + postProcess = POST_REMOVE_RGB_TRANS; + } else if (performGammaCorrection) { + postProcess = POST_GAMMA; + } else { + postProcess = POST_NONE; + } + break; + } + } + + private void parse_IEND_chunk(PNGChunk chunk) throws Exception { + // Store text strings + int textLen = textKeys.size(); + String[] textArray = new String[2 * textLen]; + for (int i = 0; i < textLen; i++) { + String key = (String)textKeys.get(i); + String val = (String)textStrings.get(i); + textArray[2 * i] = key; + textArray[2 * i + 1] = val; + if (emitProperties) { + String uniqueKey = "text_" + i + ':' + key; + properties.put(uniqueKey.toLowerCase(Locale.getDefault()), val); + } + } + if (encodeParam != null) { + encodeParam.setText(textArray); + } + + // Store compressed text strings + int ztextLen = ztextKeys.size(); + String[] ztextArray = new String[2 * ztextLen]; + for (int i = 0; i < ztextLen; i++) { + String key = (String)ztextKeys.get(i); + String val = (String)ztextStrings.get(i); + ztextArray[2 * i] = key; + ztextArray[2 * i + 1] = val; + if (emitProperties) { + String uniqueKey = "ztext_" + i + ':' + key; + properties.put(uniqueKey.toLowerCase(Locale.getDefault()), val); + } + } + if (encodeParam != null) { + encodeParam.setCompressedText(ztextArray); + } + + // Parse prior IDAT chunks + InputStream seqStream = + new SequenceInputStream(Collections.enumeration(streamVec)); + InputStream infStream = + new InflaterInputStream(seqStream, new Inflater()); + dataStream = new DataInputStream(infStream); + + // Create an empty WritableRaster + int depth = bitDepth; + if ((colorType == PNG_COLOR_GRAY) + && (bitDepth < 8) && output8BitGray) { + depth = 8; + } + if ((colorType == PNG_COLOR_PALETTE) && expandPalette) { + depth = 8; + } + int bytesPerRow = (outputBands * width * depth + 7) / 8; + int scanlineStride = + (depth == 16) ? (bytesPerRow / 2) : bytesPerRow; + + theTile = createRaster(width, height, outputBands, + scanlineStride, + depth); + + if (performGammaCorrection && (gammaLut == null)) { + initGammaLut(bitDepth); + } + if ((postProcess == POST_GRAY_LUT) + || (postProcess == POST_GRAY_LUT_ADD_TRANS) + || (postProcess == POST_GRAY_LUT_ADD_TRANS_EXP)) { + initGrayLut(bitDepth); + } + + decodeImage(interlaceMethod == 1); + sampleModel = theTile.getSampleModel(); + + if ((colorType == PNG_COLOR_PALETTE) && !expandPalette) { + if (outputHasAlphaPalette) { + colorModel = new IndexColorModel(bitDepth, + paletteEntries, + redPalette, + greenPalette, + bluePalette, + alphaPalette); + } else { + colorModel = new IndexColorModel(bitDepth, + paletteEntries, + redPalette, + greenPalette, + bluePalette); + } + } else if ((colorType == PNG_COLOR_GRAY) + && (bitDepth < 8) && !output8BitGray) { + byte[] palette = expandBits[bitDepth]; + colorModel = new IndexColorModel(bitDepth, + palette.length, + palette, + palette, + palette); + } else { + colorModel = + createComponentColorModel(sampleModel); + } + } + + private static final int[] GrayBits8 = { 8 }; + private static final ComponentColorModel colorModelGray8 = + new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_GRAY), + GrayBits8, false, false, + Transparency.OPAQUE, + DataBuffer.TYPE_BYTE); + + private static final int[] GrayAlphaBits8 = { 8, 8 }; + private static final ComponentColorModel colorModelGrayAlpha8 = + new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_GRAY), + GrayAlphaBits8, true, false, + Transparency.TRANSLUCENT, + DataBuffer.TYPE_BYTE); + + private static final int[] GrayBits16 = { 16 }; + private static final ComponentColorModel colorModelGray16 = + new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_GRAY), + GrayBits16, false, false, + Transparency.OPAQUE, + DataBuffer.TYPE_USHORT); + + private static final int[] GrayAlphaBits16 = { 16, 16 }; + private static final ComponentColorModel colorModelGrayAlpha16 = + new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_GRAY), + GrayAlphaBits16, true, false, + Transparency.TRANSLUCENT, + DataBuffer.TYPE_USHORT); + + private static final int[] GrayBits32 = { 32 }; + private static final ComponentColorModel colorModelGray32 = + new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_GRAY), + GrayBits32, false, false, + Transparency.OPAQUE, + DataBuffer.TYPE_INT); + + private static final int[] GrayAlphaBits32 = { 32, 32 }; + private static final ComponentColorModel colorModelGrayAlpha32 = + new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_GRAY), + GrayAlphaBits32, true, false, + Transparency.TRANSLUCENT, + DataBuffer.TYPE_INT); + + private static final int[] RGBBits8 = { 8, 8, 8 }; + private static final ComponentColorModel colorModelRGB8 = + new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), + RGBBits8, false, false, + Transparency.OPAQUE, + DataBuffer.TYPE_BYTE); + + private static final int[] RGBABits8 = { 8, 8, 8, 8 }; + private static final ComponentColorModel colorModelRGBA8 = + new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), + RGBABits8, true, false, + Transparency.TRANSLUCENT, + DataBuffer.TYPE_BYTE); + + private static final int[] RGBBits16 = { 16, 16, 16 }; + private static final ComponentColorModel colorModelRGB16 = + new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), + RGBBits16, false, false, + Transparency.OPAQUE, + DataBuffer.TYPE_USHORT); + + private static final int[] RGBABits16 = { 16, 16, 16, 16 }; + private static final ComponentColorModel colorModelRGBA16 = + new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), + RGBABits16, true, false, + Transparency.TRANSLUCENT, + DataBuffer.TYPE_USHORT); + + private static final int[] RGBBits32 = { 32, 32, 32 }; + private static final ComponentColorModel colorModelRGB32 = + new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), + RGBBits32, false, false, + Transparency.OPAQUE, + DataBuffer.TYPE_INT); + + private static final int[] RGBABits32 = { 32, 32, 32, 32 }; + private static final ComponentColorModel colorModelRGBA32 = + new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), + RGBABits32, true, false, + Transparency.TRANSLUCENT, + DataBuffer.TYPE_INT); + /** + * A convenience method to create an instance of + * ComponentColorModel suitable for use with the + * given SampleModel. The SampleModel + * should have a data type of DataBuffer.TYPE_BYTE, + * TYPE_USHORT, or TYPE_INT and between + * 1 and 4 bands. Depending on the number of bands of the + * SampleModel, either a gray, gray+alpha, rgb, or + * rgb+alpha ColorModel is returned. + */ + public static ColorModel createComponentColorModel(SampleModel sm) { + int type = sm.getDataType(); + int bands = sm.getNumBands(); + ComponentColorModel cm = null; + + if (type == DataBuffer.TYPE_BYTE) { + switch (bands) { + case 1: + cm = colorModelGray8; + break; + case 2: + cm = colorModelGrayAlpha8; + break; + case 3: + cm = colorModelRGB8; + break; + case 4: + cm = colorModelRGBA8; + break; + } + } else if (type == DataBuffer.TYPE_USHORT) { + switch (bands) { + case 1: + cm = colorModelGray16; + break; + case 2: + cm = colorModelGrayAlpha16; + break; + case 3: + cm = colorModelRGB16; + break; + case 4: + cm = colorModelRGBA16; + break; + } + } else if (type == DataBuffer.TYPE_INT) { + switch (bands) { + case 1: + cm = colorModelGray32; + break; + case 2: + cm = colorModelGrayAlpha32; + break; + case 3: + cm = colorModelRGB32; + break; + case 4: + cm = colorModelRGBA32; + break; + } + } + + return cm; + } + + private void parse_PLTE_chunk(PNGChunk chunk) { + paletteEntries = chunk.getLength() / 3; + redPalette = new byte[paletteEntries]; + greenPalette = new byte[paletteEntries]; + bluePalette = new byte[paletteEntries]; + + int pltIndex = 0; + + // gAMA chunk must precede PLTE chunk + if (performGammaCorrection) { + if (gammaLut == null) { + initGammaLut(bitDepth == 16 ? 16 : 8); + } + + for (int i = 0; i < paletteEntries; i++) { + byte r = chunk.getByte(pltIndex++); + byte g = chunk.getByte(pltIndex++); + byte b = chunk.getByte(pltIndex++); + + redPalette[i] = (byte)gammaLut[r & 0xff]; + greenPalette[i] = (byte)gammaLut[g & 0xff]; + bluePalette[i] = (byte)gammaLut[b & 0xff]; + } + } else { + for (int i = 0; i < paletteEntries; i++) { + redPalette[i] = chunk.getByte(pltIndex++); + greenPalette[i] = chunk.getByte(pltIndex++); + bluePalette[i] = chunk.getByte(pltIndex++); + } + } + } + + private void parse_bKGD_chunk(PNGChunk chunk) { + switch (colorType) { + case PNG_COLOR_PALETTE: + int bkgdIndex = chunk.getByte(0) & 0xff; + + bkgdRed = redPalette[bkgdIndex] & 0xff; + bkgdGreen = greenPalette[bkgdIndex] & 0xff; + bkgdBlue = bluePalette[bkgdIndex] & 0xff; + + if (encodeParam != null) { + ((PNGEncodeParam.Palette)encodeParam).setBackgroundPaletteIndex(bkgdIndex); + } + break; + case PNG_COLOR_GRAY: case PNG_COLOR_GRAY_ALPHA: + int bkgdGray = chunk.getInt2(0); + bkgdRed = bkgdGreen = bkgdBlue = bkgdGray; + + if (encodeParam != null) { + ((PNGEncodeParam.Gray)encodeParam).setBackgroundGray(bkgdGray); + } + break; + case PNG_COLOR_RGB: case PNG_COLOR_RGB_ALPHA: + bkgdRed = chunk.getInt2(0); + bkgdGreen = chunk.getInt2(2); + bkgdBlue = chunk.getInt2(4); + + int[] bkgdRGB = new int[3]; + bkgdRGB[0] = bkgdRed; + bkgdRGB[1] = bkgdGreen; + bkgdRGB[2] = bkgdBlue; + if (encodeParam != null) { + ((PNGEncodeParam.RGB)encodeParam).setBackgroundRGB(bkgdRGB); + } + break; + } + + int r = 0; + int g = 0; + int b = 0; + if (bitDepth < 8) { + r = expandBits[bitDepth][bkgdRed]; + g = expandBits[bitDepth][bkgdGreen]; + b = expandBits[bitDepth][bkgdBlue]; + } else if (bitDepth == 8) { + r = bkgdRed; + g = bkgdGreen; + b = bkgdBlue; + } else if (bitDepth == 16) { + r = bkgdRed >> 8; + g = bkgdGreen >> 8; + b = bkgdBlue >> 8; + } + if (emitProperties) { + properties.put("background_color", new Color(r, g, b)); + } + } + + private void parse_cHRM_chunk(PNGChunk chunk) { + // If an sRGB chunk exists, ignore cHRM chunks + if (sRGBRenderingIntent != -1) { + return; + } + + chromaticity = new float[8]; + chromaticity[0] = chunk.getInt4(0) / 100000.0F; + chromaticity[1] = chunk.getInt4(4) / 100000.0F; + chromaticity[2] = chunk.getInt4(8) / 100000.0F; + chromaticity[3] = chunk.getInt4(12) / 100000.0F; + chromaticity[4] = chunk.getInt4(16) / 100000.0F; + chromaticity[5] = chunk.getInt4(20) / 100000.0F; + chromaticity[6] = chunk.getInt4(24) / 100000.0F; + chromaticity[7] = chunk.getInt4(28) / 100000.0F; + + if (encodeParam != null) { + encodeParam.setChromaticity(chromaticity); + } + if (emitProperties) { + properties.put("white_point_x", chromaticity[0]); + properties.put("white_point_y", chromaticity[1]); + properties.put("red_x", chromaticity[2]); + properties.put("red_y", chromaticity[3]); + properties.put("green_x", chromaticity[4]); + properties.put("green_y", chromaticity[5]); + properties.put("blue_x", chromaticity[6]); + properties.put("blue_y", chromaticity[7]); + } + } + + private void parse_gAMA_chunk(PNGChunk chunk) { + // If an sRGB chunk exists, ignore gAMA chunks + if (sRGBRenderingIntent != -1) { + return; + } + + fileGamma = chunk.getInt4(0) / 100000.0F; + + float exp = + performGammaCorrection ? displayExponent / userExponent : 1.0F; + if (encodeParam != null) { + encodeParam.setGamma(fileGamma * exp); + } + if (emitProperties) { + properties.put("gamma", fileGamma * exp); + } + } + + private void parse_hIST_chunk(PNGChunk chunk) { + if (redPalette == null) { + String msg = PropertyUtil.getString("PNGImageDecoder18"); + throw new RuntimeException(msg); + } + + int length = redPalette.length; + int[] hist = new int[length]; + for (int i = 0; i < length; i++) { + hist[i] = chunk.getInt2(2 * i); + } + + if (encodeParam != null) { + encodeParam.setPaletteHistogram(hist); + } + } + + private void parse_iCCP_chunk(PNGChunk chunk) { +// String name = ""; // todo simplify this +// byte b; + +// int textIndex = 0; +// while ((b = chunk.getByte(textIndex++)) != 0) { +// name += (char)b; +// } + } + + private void parse_pHYs_chunk(PNGChunk chunk) { + xPixelsPerUnit = chunk.getInt4(0); + yPixelsPerUnit = chunk.getInt4(4); + unitSpecifier = chunk.getInt1(8); + + if (encodeParam != null) { + encodeParam.setPhysicalDimension(xPixelsPerUnit, + yPixelsPerUnit, + unitSpecifier); + } + if (emitProperties) { + properties.put("x_pixels_per_unit", xPixelsPerUnit); + properties.put("y_pixels_per_unit", yPixelsPerUnit); + properties.put("pixel_aspect_ratio", + (float) xPixelsPerUnit / yPixelsPerUnit); + if (unitSpecifier == 1) { + properties.put("pixel_units", "Meters"); + } else if (unitSpecifier != 0) { + // Error -- unit specifier must be 0 or 1 + String msg = PropertyUtil.getString("PNGImageDecoder12"); + throw new RuntimeException(msg); + } + } + } + + private void parse_sBIT_chunk(PNGChunk chunk) { + if (colorType == PNG_COLOR_PALETTE) { + significantBits = new int[3]; + } else { + significantBits = new int[inputBands]; + } + for (int i = 0; i < significantBits.length; i++) { + int bits = chunk.getByte(i); + int depth = (colorType == PNG_COLOR_PALETTE) ? 8 : bitDepth; + if (bits <= 0 || bits > depth) { + // Error -- significant bits must be between 0 and + // image bit depth. + String msg = PropertyUtil.getString("PNGImageDecoder13"); + throw new RuntimeException(msg); + } + significantBits[i] = bits; + } + + if (encodeParam != null) { + encodeParam.setSignificantBits(significantBits); + } + if (emitProperties) { + properties.put("significant_bits", significantBits); + } + } + + private void parse_sRGB_chunk(PNGChunk chunk) { + sRGBRenderingIntent = chunk.getByte(0); + + // The presence of an sRGB chunk implies particular + // settings for gamma and chroma. + fileGamma = 45455 / 100000.0F; + + chromaticity = new float[8]; + chromaticity[0] = 31270 / 10000.0F; + chromaticity[1] = 32900 / 10000.0F; + chromaticity[2] = 64000 / 10000.0F; + chromaticity[3] = 33000 / 10000.0F; + chromaticity[4] = 30000 / 10000.0F; + chromaticity[5] = 60000 / 10000.0F; + chromaticity[6] = 15000 / 10000.0F; + chromaticity[7] = 6000 / 10000.0F; + + if (performGammaCorrection) { + // File gamma is 1/2.2 + float gamma = fileGamma * (displayExponent / userExponent); + if (encodeParam != null) { + encodeParam.setGamma(gamma); + encodeParam.setChromaticity(chromaticity); + } + if (emitProperties) { + properties.put("gamma", gamma); + properties.put("white_point_x", chromaticity[0]); + properties.put("white_point_y", chromaticity[1]); + properties.put("red_x", chromaticity[2]); + properties.put("red_y", chromaticity[3]); + properties.put("green_x", chromaticity[4]); + properties.put("green_y", chromaticity[5]); + properties.put("blue_x", chromaticity[6]); + properties.put("blue_y", chromaticity[7]); + } + } + } + + private void parse_tEXt_chunk(PNGChunk chunk) { + + byte b; + StringBuffer key = new StringBuffer(); + int textIndex = 0; + while ((b = chunk.getByte(textIndex++)) != 0) { + key.append((char)b); + } + + StringBuilder value = new StringBuilder(); + for (int i = textIndex; i < chunk.getLength(); i++) { + value.append((char)chunk.getByte(i)); + } + + textKeys.add(key.toString()); + textStrings.add(value.toString()); + } + + private void parse_tIME_chunk(PNGChunk chunk) { + int year = chunk.getInt2(0); + int month = chunk.getInt1(2) - 1; + int day = chunk.getInt1(3); + int hour = chunk.getInt1(4); + int minute = chunk.getInt1(5); + int second = chunk.getInt1(6); + + TimeZone gmt = TimeZone.getTimeZone("GMT"); + + GregorianCalendar cal = new GregorianCalendar(gmt); + cal.set(year, month, day, + hour, minute, second); + Date date = cal.getTime(); + + if (encodeParam != null) { + encodeParam.setModificationTime(date); + } + if (emitProperties) { + properties.put("timestamp", date); + } + } + + private void parse_tRNS_chunk(PNGChunk chunk) { + if (colorType == PNG_COLOR_PALETTE) { + int entries = chunk.getLength(); + if (entries > paletteEntries) { + // Error -- mustn't have more alpha than RGB palette entries + String msg = PropertyUtil.getString("PNGImageDecoder14"); + throw new RuntimeException(msg); + } + + // Load beginning of palette from the chunk + alphaPalette = new byte[paletteEntries]; + for (int i = 0; i < entries; i++) { + alphaPalette[i] = chunk.getByte(i); + } + + // Fill rest of palette with 255 + for (int i = entries; i < paletteEntries; i++) { + alphaPalette[i] = (byte)255; + } + + if (!suppressAlpha) { + if (expandPalette) { + postProcess = POST_PALETTE_TO_RGBA; + outputBands = 4; + } else { + outputHasAlphaPalette = true; + } + } + } else if (colorType == PNG_COLOR_GRAY) { + grayTransparentAlpha = chunk.getInt2(0); + + if (!suppressAlpha) { + if (bitDepth < 8) { + output8BitGray = true; + maxOpacity = 255; + postProcess = POST_GRAY_LUT_ADD_TRANS; + } else { + postProcess = POST_ADD_GRAY_TRANS; + } + + if (expandGrayAlpha) { + outputBands = 4; + postProcess |= POST_EXP_MASK; + } else { + outputBands = 2; + } + + if (encodeParam != null) { + ((PNGEncodeParam.Gray)encodeParam).setTransparentGray(grayTransparentAlpha); + } + } + } else if (colorType == PNG_COLOR_RGB) { + redTransparentAlpha = chunk.getInt2(0); + greenTransparentAlpha = chunk.getInt2(2); + blueTransparentAlpha = chunk.getInt2(4); + + if (!suppressAlpha) { + outputBands = 4; + postProcess = POST_ADD_RGB_TRANS; + + if (encodeParam != null) { + int[] rgbTrans = new int[3]; + rgbTrans[0] = redTransparentAlpha; + rgbTrans[1] = greenTransparentAlpha; + rgbTrans[2] = blueTransparentAlpha; + ((PNGEncodeParam.RGB)encodeParam).setTransparentRGB(rgbTrans); + } + } + } else if (colorType == PNG_COLOR_GRAY_ALPHA + || colorType == PNG_COLOR_RGB_ALPHA) { + // Error -- GA or RGBA image can't have a tRNS chunk. + String msg = PropertyUtil.getString("PNGImageDecoder15"); + throw new RuntimeException(msg); + } + } + + private void parse_zTXt_chunk(PNGChunk chunk) { + + int textIndex = 0; + StringBuffer key = new StringBuffer(); + byte b; + while ((b = chunk.getByte(textIndex++)) != 0) { + key.append((char)b); + } + + // skip method + textIndex++; + + StringBuffer value = new StringBuffer(); + try { + int length = chunk.getLength() - textIndex; + byte[] data = chunk.getData(); + InputStream cis = + new ByteArrayInputStream(data, textIndex, length); + InputStream iis = new InflaterInputStream(cis); + + int c; + while ((c = iis.read()) != -1) { + value.append((char)c); + } + + ztextKeys.add(key.toString()); + ztextStrings.add(value.toString()); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private WritableRaster createRaster(int width, int height, int bands, + int scanlineStride, + int bitDepth) { + + DataBuffer dataBuffer; + WritableRaster ras = null; + Point origin = new Point(0, 0); + if ((bitDepth < 8) && (bands == 1)) { + dataBuffer = new DataBufferByte(height * scanlineStride); + ras = Raster.createPackedRaster(dataBuffer, + width, height, + bitDepth, + origin); + } else if (bitDepth <= 8) { + dataBuffer = new DataBufferByte(height * scanlineStride); + ras = Raster.createInterleavedRaster(dataBuffer, + width, height, + scanlineStride, + bands, + bandOffsets[bands], + origin); + } else { + dataBuffer = new DataBufferUShort(height * scanlineStride); + ras = Raster.createInterleavedRaster(dataBuffer, + width, height, + scanlineStride, + bands, + bandOffsets[bands], + origin); + } + + return ras; + } + + // Data filtering methods + + private static void decodeSubFilter(byte[] curr, int count, int bpp) { + for (int i = bpp; i < count; i++) { + int val; + + val = curr[i] & 0xff; + val += curr[i - bpp] & 0xff; + + curr[i] = (byte)val; + } + } + + private static void decodeUpFilter(byte[] curr, byte[] prev, + int count) { + for (int i = 0; i < count; i++) { + int raw = curr[i] & 0xff; + int prior = prev[i] & 0xff; + + curr[i] = (byte)(raw + prior); + } + } + + private static void decodeAverageFilter(byte[] curr, byte[] prev, + int count, int bpp) { + int raw; + int priorPixel; + int priorRow; + + for (int i = 0; i < bpp; i++) { + raw = curr[i] & 0xff; + priorRow = prev[i] & 0xff; + + curr[i] = (byte)(raw + priorRow / 2); + } + + for (int i = bpp; i < count; i++) { + raw = curr[i] & 0xff; + priorPixel = curr[i - bpp] & 0xff; + priorRow = prev[i] & 0xff; + + curr[i] = (byte)(raw + (priorPixel + priorRow) / 2); + } + } + + private static void decodePaethFilter(byte[] curr, byte[] prev, + int count, int bpp) { + int raw; + int priorPixel; + int priorRow; + int priorRowPixel; + + for (int i = 0; i < bpp; i++) { + raw = curr[i] & 0xff; + priorRow = prev[i] & 0xff; + + curr[i] = (byte)(raw + priorRow); + } + + for (int i = bpp; i < count; i++) { + raw = curr[i] & 0xff; + priorPixel = curr[i - bpp] & 0xff; + priorRow = prev[i] & 0xff; + priorRowPixel = prev[i - bpp] & 0xff; + + curr[i] = (byte)(raw + PNGEncodeParam.paethPredictor(priorPixel, + priorRow, + priorRowPixel)); + } + } + + private void processPixels(int process, + Raster src, WritableRaster dst, + int xOffset, int step, int y, int width) { + int srcX; + int dstX; + + // Create an array suitable for holding one pixel + int[] ps = src.getPixel(0, 0, (int[])null); + int[] pd = dst.getPixel(0, 0, (int[])null); + + dstX = xOffset; + switch (process) { + case POST_NONE: + for (srcX = 0; srcX < width; srcX++) { + src.getPixel(srcX, 0, ps); + dst.setPixel(dstX, y, ps); + dstX += step; + } + break; + + case POST_GAMMA: + for (srcX = 0; srcX < width; srcX++) { + src.getPixel(srcX, 0, ps); + + for (int i = 0; i < inputBands; i++) { + int x = ps[i]; + ps[i] = gammaLut[x]; + } + + dst.setPixel(dstX, y, ps); + dstX += step; + } + break; + + case POST_GRAY_LUT: + for (srcX = 0; srcX < width; srcX++) { + src.getPixel(srcX, 0, ps); + + pd[0] = grayLut[ps[0]]; + + dst.setPixel(dstX, y, pd); + dstX += step; + } + break; + + case POST_GRAY_LUT_ADD_TRANS: + for (srcX = 0; srcX < width; srcX++) { + src.getPixel(srcX, 0, ps); + + int val = ps[0]; + pd[0] = grayLut[val]; + if (val == grayTransparentAlpha) { + pd[1] = 0; + } else { + pd[1] = maxOpacity; + } + + dst.setPixel(dstX, y, pd); + dstX += step; + } + break; + + case POST_PALETTE_TO_RGB: + for (srcX = 0; srcX < width; srcX++) { + src.getPixel(srcX, 0, ps); + + int val = ps[0]; + pd[0] = redPalette[val]; + pd[1] = greenPalette[val]; + pd[2] = bluePalette[val]; + + dst.setPixel(dstX, y, pd); + dstX += step; + } + break; + + case POST_PALETTE_TO_RGBA: + for (srcX = 0; srcX < width; srcX++) { + src.getPixel(srcX, 0, ps); + + int val = ps[0]; + pd[0] = redPalette[val]; + pd[1] = greenPalette[val]; + pd[2] = bluePalette[val]; + pd[3] = alphaPalette[val]; + + dst.setPixel(dstX, y, pd); + dstX += step; + } + break; + + case POST_ADD_GRAY_TRANS: + for (srcX = 0; srcX < width; srcX++) { + src.getPixel(srcX, 0, ps); + + int val = ps[0]; + if (performGammaCorrection) { + val = gammaLut[val]; + } + pd[0] = val; + if (val == grayTransparentAlpha) { + pd[1] = 0; + } else { + pd[1] = maxOpacity; + } + + dst.setPixel(dstX, y, pd); + dstX += step; + } + break; + + case POST_ADD_RGB_TRANS: + for (srcX = 0; srcX < width; srcX++) { + src.getPixel(srcX, 0, ps); + + int r = ps[0]; + int g = ps[1]; + int b = ps[2]; + if (performGammaCorrection) { + pd[0] = gammaLut[r]; + pd[1] = gammaLut[g]; + pd[2] = gammaLut[b]; + } else { + pd[0] = r; + pd[1] = g; + pd[2] = b; + } + if ((r == redTransparentAlpha) + && (g == greenTransparentAlpha) + && (b == blueTransparentAlpha)) { + pd[3] = 0; + } else { + pd[3] = maxOpacity; + } + + dst.setPixel(dstX, y, pd); + dstX += step; + } + break; + + case POST_REMOVE_GRAY_TRANS: + for (srcX = 0; srcX < width; srcX++) { + src.getPixel(srcX, 0, ps); + + int g = ps[0]; + if (performGammaCorrection) { + pd[0] = gammaLut[g]; + } else { + pd[0] = g; + } + + dst.setPixel(dstX, y, pd); + dstX += step; + } + break; + + case POST_REMOVE_RGB_TRANS: + for (srcX = 0; srcX < width; srcX++) { + src.getPixel(srcX, 0, ps); + + int r = ps[0]; + int g = ps[1]; + int b = ps[2]; + if (performGammaCorrection) { + pd[0] = gammaLut[r]; + pd[1] = gammaLut[g]; + pd[2] = gammaLut[b]; + } else { + pd[0] = r; + pd[1] = g; + pd[2] = b; + } + + dst.setPixel(dstX, y, pd); + dstX += step; + } + break; + + case POST_GAMMA_EXP: + for (srcX = 0; srcX < width; srcX++) { + src.getPixel(srcX, 0, ps); + + int val = ps[0]; + int alpha = ps[1]; + int gamma = gammaLut[val]; + pd[0] = gamma; + pd[1] = gamma; + pd[2] = gamma; + pd[3] = alpha; + + dst.setPixel(dstX, y, pd); + dstX += step; + } + break; + + case POST_GRAY_ALPHA_EXP: + for (srcX = 0; srcX < width; srcX++) { + src.getPixel(srcX, 0, ps); + + int val = ps[0]; + int alpha = ps[1]; + pd[0] = val; + pd[1] = val; + pd[2] = val; + pd[3] = alpha; + + dst.setPixel(dstX, y, pd); + dstX += step; + } + break; + + case POST_ADD_GRAY_TRANS_EXP: + for (srcX = 0; srcX < width; srcX++) { + src.getPixel(srcX, 0, ps); + + int val = ps[0]; + if (performGammaCorrection) { + val = gammaLut[val]; + } + pd[0] = val; + pd[1] = val; + pd[2] = val; + if (val == grayTransparentAlpha) { + pd[3] = 0; + } else { + pd[3] = maxOpacity; + } + + dst.setPixel(dstX, y, pd); + dstX += step; + } + break; + + case POST_GRAY_LUT_ADD_TRANS_EXP: + for (srcX = 0; srcX < width; srcX++) { + src.getPixel(srcX, 0, ps); + + int val = ps[0]; + int val2 = grayLut[val]; + pd[0] = val2; + pd[1] = val2; + pd[2] = val2; + if (val == grayTransparentAlpha) { + pd[3] = 0; + } else { + pd[3] = maxOpacity; + } + + dst.setPixel(dstX, y, pd); + dstX += step; + } + break; + } + } + + /** + * Reads in an image of a given size and returns it as a + * WritableRaster. + */ + private void decodePass(WritableRaster imRas, + int xOffset, int yOffset, + int xStep, int yStep, + int passWidth, int passHeight) { + if ((passWidth == 0) || (passHeight == 0)) { + return; + } + + int bytesPerRow = (inputBands * passWidth * bitDepth + 7) / 8; + int eltsPerRow = (bitDepth == 16) ? bytesPerRow / 2 : bytesPerRow; + byte[] curr = new byte[bytesPerRow]; + byte[] prior = new byte[bytesPerRow]; + + // Create a 1-row tall Raster to hold the data + WritableRaster passRow = + createRaster(passWidth, 1, inputBands, + eltsPerRow, + bitDepth); + DataBuffer dataBuffer = passRow.getDataBuffer(); + int type = dataBuffer.getDataType(); + byte[] byteData = null; + short[] shortData = null; + if (type == DataBuffer.TYPE_BYTE) { + byteData = ((DataBufferByte)dataBuffer).getData(); + } else { + shortData = ((DataBufferUShort)dataBuffer).getData(); + } + + // Decode the (sub)image row-by-row + int srcY; + int dstY; + for (srcY = 0, dstY = yOffset; + srcY < passHeight; + srcY++, dstY += yStep) { + // Read the filter type byte and a row of data + int filter = 0; + try { + filter = dataStream.read(); + dataStream.readFully(curr, 0, bytesPerRow); + } catch (Exception e) { + e.printStackTrace(); + } + + switch (filter) { + case PNG_FILTER_NONE: + break; + case PNG_FILTER_SUB: + decodeSubFilter(curr, bytesPerRow, bytesPerPixel); + break; + case PNG_FILTER_UP: + decodeUpFilter(curr, prior, bytesPerRow); + break; + case PNG_FILTER_AVERAGE: + decodeAverageFilter(curr, prior, bytesPerRow, bytesPerPixel); + break; + case PNG_FILTER_PAETH: + decodePaethFilter(curr, prior, bytesPerRow, bytesPerPixel); + break; + default: + // Error -- uknown filter type + String msg = PropertyUtil.getString("PNGImageDecoder16"); + throw new RuntimeException(msg); + } + + // Copy data into passRow byte by byte + if (bitDepth < 16) { + System.arraycopy(curr, 0, byteData, 0, bytesPerRow); + } else { + int idx = 0; + for (int j = 0; j < eltsPerRow; j++) { + shortData[j] = + (short)((curr[idx] << 8) | (curr[idx + 1] & 0xff)); + idx += 2; + } + } + + processPixels(postProcess, + passRow, imRas, xOffset, xStep, dstY, passWidth); + + // Swap curr and prior + byte[] tmp = prior; + prior = curr; + curr = tmp; + } + } + + private void decodeImage(boolean useInterlacing) { + if (!useInterlacing) { + decodePass(theTile, 0, 0, 1, 1, width, height); + } else { + decodePass(theTile, 0, 0, 8, 8, (width + 7) / 8, (height + 7) / 8); + decodePass(theTile, 4, 0, 8, 8, (width + 3) / 8, (height + 7) / 8); + decodePass(theTile, 0, 4, 4, 8, (width + 3) / 4, (height + 3) / 8); + decodePass(theTile, 2, 0, 4, 4, (width + 1) / 4, (height + 3) / 4); + decodePass(theTile, 0, 2, 2, 4, (width + 1) / 2, (height + 1) / 4); + decodePass(theTile, 1, 0, 2, 2, width / 2, (height + 1) / 2); + decodePass(theTile, 0, 1, 1, 2, width, height / 2); + } + } + + // RenderedImage stuff + + public Raster getTile(int tileX, int tileY) { + if (tileX != 0 || tileY != 0) { + // Error -- bad tile requested + String msg = PropertyUtil.getString("PNGImageDecoder17"); + throw new IllegalArgumentException(msg); + } + return theTile; + } +} diff --git a/src/main/java/org/apache/xmlgraphics/image/codec/png/PNGImageEncoder.java b/src/main/java/org/apache/xmlgraphics/image/codec/png/PNGImageEncoder.java new file mode 100644 index 0000000..159270f --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/codec/png/PNGImageEncoder.java @@ -0,0 +1,1107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PNGImageEncoder.java 1804124 2017-08-04 14:13:54Z ssteiner $ */ + +package org.apache.xmlgraphics.image.codec.png; + +import java.awt.Rectangle; +import java.awt.image.ColorModel; +import java.awt.image.IndexColorModel; +import java.awt.image.Raster; +import java.awt.image.RenderedImage; +import java.awt.image.SampleModel; +import java.io.ByteArrayOutputStream; +import java.io.DataOutput; +import java.io.DataOutputStream; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.TimeZone; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; + +import org.apache.xmlgraphics.image.codec.util.ImageEncoderImpl; +import org.apache.xmlgraphics.image.codec.util.PropertyUtil; + +// CSOFF: ConstantName +// CSOFF: InnerAssignment +// CSOFF: LocalVariableName +// CSOFF: MissingSwitchDefault +// CSOFF: OperatorWrap +// CSOFF: WhitespaceAround + +final class CRC { + + private CRC() { + } + + private static int[] crcTable = new int[256]; + + static { + // Initialize CRC table + for (int n = 0; n < 256; n++) { + int c = n; + for (int k = 0; k < 8; k++) { + if ((c & 1) == 1) { + c = 0xedb88320 ^ (c >>> 1); + } else { + c >>>= 1; + } + + crcTable[n] = c; + } + } + } + + public static int updateCRC(int crc, byte[] data, int off, int len) { + int c = crc; + + for (int n = 0; n < len; n++) { + c = crcTable[(c ^ data[off + n]) & 0xff] ^ (c >>> 8); + } + + return c; + } +} + + +class ChunkStream extends OutputStream implements DataOutput { + + private String type; + private ByteArrayOutputStream baos; + private DataOutputStream dos; + + ChunkStream(String type) throws IOException { + this.type = type; + + this.baos = new ByteArrayOutputStream(); + this.dos = new DataOutputStream(baos); + } + + @Override + public void write(byte[] b) throws IOException { + dos.write(b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + dos.write(b, off, len); + } + + @Override + public void write(int b) throws IOException { + dos.write(b); + } + + public void writeBoolean(boolean v) throws IOException { + dos.writeBoolean(v); + } + + public void writeByte(int v) throws IOException { + dos.writeByte(v); + } + + public void writeBytes(String s) throws IOException { + dos.writeBytes(s); + } + + public void writeChar(int v) throws IOException { + dos.writeChar(v); + } + + public void writeChars(String s) throws IOException { + dos.writeChars(s); + } + + public void writeDouble(double v) throws IOException { + dos.writeDouble(v); + } + + public void writeFloat(float v) throws IOException { + dos.writeFloat(v); + } + + public void writeInt(int v) throws IOException { + dos.writeInt(v); + } + + public void writeLong(long v) throws IOException { + dos.writeLong(v); + } + + public void writeShort(int v) throws IOException { + dos.writeShort(v); + } + + public void writeUTF(String str) throws IOException { + dos.writeUTF(str); + } + + public void writeToStream(DataOutputStream output) throws IOException { + byte[] typeSignature = new byte[4]; + typeSignature[0] = (byte)type.charAt(0); + typeSignature[1] = (byte)type.charAt(1); + typeSignature[2] = (byte)type.charAt(2); + typeSignature[3] = (byte)type.charAt(3); + + dos.flush(); + baos.flush(); + + byte[] data = baos.toByteArray(); + int len = data.length; + + output.writeInt(len); + output.write(typeSignature); + output.write(data, 0, len); + + int crc = 0xffffffff; + crc = CRC.updateCRC(crc, typeSignature, 0, 4); + crc = CRC.updateCRC(crc, data, 0, len); + output.writeInt(crc ^ 0xffffffff); + } + + /** {@inheritDoc} */ + @Override + public void close() throws IOException { + + if (baos != null) { + baos.close(); + baos = null; + } + if (dos != null) { + dos.close(); + dos = null; + } + } +} + + +class IDATOutputStream extends FilterOutputStream { + + private static final byte[] TYPE_SIGNATURE + = {(byte)'I', (byte)'D', (byte)'A', (byte)'T'}; + + private int bytesWritten; + private int segmentLength; + private byte[] buffer; + + public IDATOutputStream(OutputStream output, + int segmentLength) { + super(output); + this.segmentLength = segmentLength; + this.buffer = new byte[segmentLength]; + } + + @Override + public void close() throws IOException { + flush(); + } + + private void writeInt(int x) throws IOException { + out.write(x >> 24); + out.write((x >> 16) & 0xff); + out.write((x >> 8) & 0xff); + out.write(x & 0xff); + } + + @Override + public void flush() throws IOException { + if (bytesWritten == 0) { + return; + } + + // Length + writeInt(bytesWritten); + // 'IDAT' signature + out.write(TYPE_SIGNATURE); + // Data + out.write(buffer, 0, bytesWritten); + + int crc = 0xffffffff; + crc = CRC.updateCRC(crc, TYPE_SIGNATURE, 0, 4); + crc = CRC.updateCRC(crc, buffer, 0, bytesWritten); + + // CRC + writeInt(crc ^ 0xffffffff); + + // Reset buffer + bytesWritten = 0; + } + + @Override + public void write(byte[] b) throws IOException { + this.write(b, 0, b.length); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + while (len > 0) { + int bytes = Math.min(segmentLength - bytesWritten, len); + System.arraycopy(b, off, buffer, bytesWritten, bytes); + off += bytes; + len -= bytes; + bytesWritten += bytes; + + if (bytesWritten == segmentLength) { + flush(); + } + } + } + + @Override + public void write(int b) throws IOException { + buffer[bytesWritten++] = (byte)b; + if (bytesWritten == segmentLength) { + flush(); + } + } +} + +/** + * An ImageEncoder for the PNG file format. + * + * @since EA4 + */ +public class PNGImageEncoder extends ImageEncoderImpl { + + private static final int PNG_COLOR_GRAY = 0; + private static final int PNG_COLOR_RGB = 2; + private static final int PNG_COLOR_PALETTE = 3; + private static final int PNG_COLOR_GRAY_ALPHA = 4; + private static final int PNG_COLOR_RGB_ALPHA = 6; + + private static final byte[] MAGIC = { + (byte)137, (byte) 80, (byte) 78, (byte) 71, + (byte) 13, (byte) 10, (byte) 26, (byte) 10 + }; + + private PNGEncodeParam param; + + private RenderedImage image; + private int width; + private int height; + private int bitDepth; + private int bitShift; + private int numBands; + private int colorType; + + private int bpp; // bytes per pixel, rounded up + + private boolean skipAlpha; + private boolean compressGray; + + private boolean interlace; + + private byte[] redPalette; + private byte[] greenPalette; + private byte[] bluePalette; + private byte[] alphaPalette; + + private DataOutputStream dataOutput; + + public PNGImageEncoder(OutputStream output, + PNGEncodeParam param) { + super(output, param); + + if (param != null) { + this.param = param; + } + this.dataOutput = new DataOutputStream(output); + } + + private void writeMagic() throws IOException { + dataOutput.write(MAGIC); + } + + private void writeIHDR() throws IOException { + ChunkStream cs = new ChunkStream("IHDR"); + try { + cs.writeInt(width); + cs.writeInt(height); + cs.writeByte((byte)bitDepth); + cs.writeByte((byte)colorType); + cs.writeByte((byte)0); + cs.writeByte((byte)0); + cs.writeByte(interlace ? (byte)1 : (byte)0); + + cs.writeToStream(dataOutput); + } finally { + cs.close(); + } + } + + private byte[] prevRow; + private byte[] currRow; + + private byte[][] filteredRows; + + private static int clamp(int val, int maxValue) { + return (val > maxValue) ? maxValue : val; + } + + private void encodePass(OutputStream os, Raster ras, + int xOffset, int yOffset, + int xSkip, int ySkip) + throws IOException { + int minX = ras.getMinX(); + int minY = ras.getMinY(); + int width = ras.getWidth(); + int height = ras.getHeight(); + + xOffset *= numBands; + xSkip *= numBands; + + int samplesPerByte = 8 / bitDepth; + + int numSamples = width * numBands; + int[] samples = new int[numSamples]; + + int pixels = (numSamples - xOffset + xSkip - 1) / xSkip; + int bytesPerRow = pixels * numBands; + if (bitDepth < 8) { + bytesPerRow = (bytesPerRow + samplesPerByte - 1) / samplesPerByte; + } else if (bitDepth == 16) { + bytesPerRow *= 2; + } + + if (bytesPerRow == 0) { + return; + } + + currRow = new byte[bytesPerRow + bpp]; + prevRow = new byte[bytesPerRow + bpp]; + + filteredRows = new byte[5][bytesPerRow + bpp]; + + int maxValue = (1 << bitDepth) - 1; + + for (int row = minY + yOffset; row < minY + height; row += ySkip) { + ras.getPixels(minX, row, width, 1, samples); + + if (compressGray) { + int shift = 8 - bitDepth; + for (int i = 0; i < width; i++) { + samples[i] >>= shift; + } + } + + int count = bpp; // leave first 'bpp' bytes zero + int pos = 0; + int tmp = 0; + + switch (bitDepth) { + case 1: case 2: case 4: + // Image can only have a single band + + int mask = samplesPerByte - 1; + for (int s = xOffset; s < numSamples; s += xSkip) { + int val = clamp(samples[s] >> bitShift, maxValue); + tmp = (tmp << bitDepth) | val; + + if (pos++ == mask) { + currRow[count++] = (byte)tmp; + tmp = 0; + pos = 0; + } + } + + // Left shift the last byte + if (pos != 0) { + tmp <<= (samplesPerByte - pos) * bitDepth; + currRow[count++] = (byte)tmp; + } + break; + + case 8: + for (int s = xOffset; s < numSamples; s += xSkip) { + for (int b = 0; b < numBands; b++) { + currRow[count++] = + (byte)clamp(samples[s + b] >> bitShift, maxValue); + } + } + break; + + case 16: + for (int s = xOffset; s < numSamples; s += xSkip) { + for (int b = 0; b < numBands; b++) { + int val = clamp(samples[s + b] >> bitShift, maxValue); + currRow[count++] = (byte)(val >> 8); + currRow[count++] = (byte)(val & 0xff); + } + } + break; + } + + // Perform filtering + int filterType = param.filterRow(currRow, prevRow, + filteredRows, + bytesPerRow, bpp); + + os.write(filterType); + os.write(filteredRows[filterType], bpp, bytesPerRow); + + // Swap current and previous rows + byte[] swap = currRow; + currRow = prevRow; + prevRow = swap; + } + } + + private void writeIDAT() throws IOException { + IDATOutputStream ios = new IDATOutputStream(dataOutput, 8192); + DeflaterOutputStream dos = + new DeflaterOutputStream(ios, new Deflater(9)); + + // Future work - don't convert entire image to a Raster It + // might seem that you could just call image.getData() but + // 'BufferedImage.subImage' doesn't appear to set the Width + // and height properly of the Child Raster, so the Raster + // you get back here appears larger than it should. + // This solves that problem by bounding the raster to the + // image's bounds... + Raster ras = image.getData(new Rectangle(image.getMinX(), + image.getMinY(), + image.getWidth(), + image.getHeight())); + // System.out.println("Image: [" + + // image.getMinY() + ", " + + // image.getMinX() + ", " + + // image.getWidth() + ", " + + // image.getHeight() + "]"); + // System.out.println("Ras: [" + + // ras.getMinX() + ", " + + // ras.getMinY() + ", " + + // ras.getWidth() + ", " + + // ras.getHeight() + "]"); + + if (skipAlpha) { + int numBands = ras.getNumBands() - 1; + int[] bandList = new int[numBands]; + for (int i = 0; i < numBands; i++) { + bandList[i] = i; + } + ras = ras.createChild(0, 0, + ras.getWidth(), ras.getHeight(), + 0, 0, + bandList); + } + + if (interlace) { + // Interlacing pass 1 + encodePass(dos, ras, 0, 0, 8, 8); + // Interlacing pass 2 + encodePass(dos, ras, 4, 0, 8, 8); + // Interlacing pass 3 + encodePass(dos, ras, 0, 4, 4, 8); + // Interlacing pass 4 + encodePass(dos, ras, 2, 0, 4, 4); + // Interlacing pass 5 + encodePass(dos, ras, 0, 2, 2, 4); + // Interlacing pass 6 + encodePass(dos, ras, 1, 0, 2, 2); + // Interlacing pass 7 + encodePass(dos, ras, 0, 1, 1, 2); + } else { + encodePass(dos, ras, 0, 0, 1, 1); + } + + dos.finish(); + dos.close(); + ios.flush(); + ios.close(); + } + + private void writeIEND() throws IOException { + ChunkStream cs = new ChunkStream("IEND"); + try { + cs.writeToStream(dataOutput); + } finally { + cs.close(); + } + } + + private static final float[] SRGB_CHROMA = { + 0.31270F, 0.329F, 0.64F, 0.33F, 0.3F, 0.6F, 0.15F, 0.06F + }; + + private void writeCHRM() throws IOException { + if (param.isChromaticitySet() || param.isSRGBIntentSet()) { + ChunkStream cs = new ChunkStream("cHRM"); + try { + float[] chroma; + if (!param.isSRGBIntentSet()) { + chroma = param.getChromaticity(); + } else { + chroma = SRGB_CHROMA; // SRGB chromaticities + } + + for (int i = 0; i < 8; i++) { + cs.writeInt((int)(chroma[i] * 100000)); + } + cs.writeToStream(dataOutput); + } finally { + cs.close(); + } + } + } + + private void writeGAMA() throws IOException { + if (param.isGammaSet() || param.isSRGBIntentSet()) { + ChunkStream cs = new ChunkStream("gAMA"); + try { + float gamma; + if (!param.isSRGBIntentSet()) { + gamma = param.getGamma(); + } else { + gamma = 1.0F / 2.2F; // SRGB gamma + } + // TD should include the .5 but causes regard to say + // everything is different. + cs.writeInt((int)(gamma * 100000/*+0.5*/)); + cs.writeToStream(dataOutput); + } finally { + cs.close(); + } + } + } + + private void writeICCP() throws IOException { + if (param.isICCProfileDataSet()) { + ChunkStream cs = new ChunkStream("iCCP"); + try { + byte[] iccProfileData = param.getICCProfileData(); + cs.write(iccProfileData); + cs.writeToStream(dataOutput); + } finally { + cs.close(); + } + } + } + + private void writeSBIT() throws IOException { + if (param.isSignificantBitsSet()) { + ChunkStream cs = new ChunkStream("sBIT"); + try { + int[] significantBits = param.getSignificantBits(); + for (int significantBit : significantBits) { + cs.writeByte(significantBit); + } + cs.writeToStream(dataOutput); + } finally { + cs.close(); + } + } + } + + private void writeSRGB() throws IOException { + if (param.isSRGBIntentSet()) { + ChunkStream cs = new ChunkStream("sRGB"); + try { + int intent = param.getSRGBIntent(); + cs.write(intent); + cs.writeToStream(dataOutput); + } finally { + cs.close(); + } + } + } + + private void writePLTE() throws IOException { + if (redPalette == null) { + return; + } + + ChunkStream cs = new ChunkStream("PLTE"); + try { + for (int i = 0; i < redPalette.length; i++) { + cs.writeByte(redPalette[i]); + cs.writeByte(greenPalette[i]); + cs.writeByte(bluePalette[i]); + } + + cs.writeToStream(dataOutput); + } finally { + cs.close(); + } + } + + private void writeBKGD() throws IOException { + if (param.isBackgroundSet()) { + ChunkStream cs = new ChunkStream("bKGD"); + try { + switch (colorType) { + case PNG_COLOR_GRAY: + case PNG_COLOR_GRAY_ALPHA: + int gray = ((PNGEncodeParam.Gray)param).getBackgroundGray(); + cs.writeShort(gray); + break; + + case PNG_COLOR_PALETTE: + int index = + ((PNGEncodeParam.Palette)param).getBackgroundPaletteIndex(); + cs.writeByte(index); + break; + + case PNG_COLOR_RGB: + case PNG_COLOR_RGB_ALPHA: + int[] rgb = ((PNGEncodeParam.RGB)param).getBackgroundRGB(); + cs.writeShort(rgb[0]); + cs.writeShort(rgb[1]); + cs.writeShort(rgb[2]); + break; + } + + cs.writeToStream(dataOutput); + } finally { + cs.close(); + } + } + } + + private void writeHIST() throws IOException { + if (param.isPaletteHistogramSet()) { + ChunkStream cs = new ChunkStream("hIST"); + try { + int[] hist = param.getPaletteHistogram(); + for (int aHist : hist) { + cs.writeShort(aHist); + } + + cs.writeToStream(dataOutput); + } finally { + cs.close(); + } + } + } + + private void writeTRNS() throws IOException { + if (param.isTransparencySet() + && (colorType != PNG_COLOR_GRAY_ALPHA) + && (colorType != PNG_COLOR_RGB_ALPHA)) { + ChunkStream cs = new ChunkStream("tRNS"); + try { + if (param instanceof PNGEncodeParam.Palette) { + byte[] t = + ((PNGEncodeParam.Palette)param).getPaletteTransparency(); + for (byte aT : t) { + cs.writeByte(aT); + } + } else if (param instanceof PNGEncodeParam.Gray) { + int t = ((PNGEncodeParam.Gray)param).getTransparentGray(); + cs.writeShort(t); + } else if (param instanceof PNGEncodeParam.RGB) { + int[] t = ((PNGEncodeParam.RGB)param).getTransparentRGB(); + cs.writeShort(t[0]); + cs.writeShort(t[1]); + cs.writeShort(t[2]); + } + + cs.writeToStream(dataOutput); + } finally { + cs.close(); + } + } else if (colorType == PNG_COLOR_PALETTE) { + int lastEntry = Math.min(255, alphaPalette.length - 1); + int nonOpaque; + for (nonOpaque = lastEntry; nonOpaque >= 0; nonOpaque--) { + if (alphaPalette[nonOpaque] != (byte)255) { + break; + } + } + + if (nonOpaque >= 0) { + ChunkStream cs = new ChunkStream("tRNS"); + try { + for (int i = 0; i <= nonOpaque; i++) { + cs.writeByte(alphaPalette[i]); + } + cs.writeToStream(dataOutput); + } finally { + cs.close(); + } + } + } + } + + private void writePHYS() throws IOException { + if (param.isPhysicalDimensionSet()) { + ChunkStream cs = new ChunkStream("pHYs"); + try { + int[] dims = param.getPhysicalDimension(); + cs.writeInt(dims[0]); + cs.writeInt(dims[1]); + cs.writeByte((byte)dims[2]); + + cs.writeToStream(dataOutput); + } finally { + cs.close(); + } + } + } + + private void writeSPLT() throws IOException { + if (param.isSuggestedPaletteSet()) { + ChunkStream cs = new ChunkStream("sPLT"); + try { + System.out.println("sPLT not supported yet."); + + cs.writeToStream(dataOutput); + } finally { + cs.close(); + } + } + } + + private void writeTIME() throws IOException { + if (param.isModificationTimeSet()) { + ChunkStream cs = new ChunkStream("tIME"); + try { + Date date = param.getModificationTime(); + TimeZone gmt = TimeZone.getTimeZone("GMT"); + + GregorianCalendar cal = new GregorianCalendar(gmt); + cal.setTime(date); + + int year = cal.get(Calendar.YEAR); + int month = cal.get(Calendar.MONTH); + int day = cal.get(Calendar.DAY_OF_MONTH); + int hour = cal.get(Calendar.HOUR_OF_DAY); + int minute = cal.get(Calendar.MINUTE); + int second = cal.get(Calendar.SECOND); + + cs.writeShort(year); + cs.writeByte(month + 1); + cs.writeByte(day); + cs.writeByte(hour); + cs.writeByte(minute); + cs.writeByte(second); + + cs.writeToStream(dataOutput); + } finally { + cs.close(); + } + } + } + + private void writeTEXT() throws IOException { + if (param.isTextSet()) { + String[] text = param.getText(); + + for (int i = 0; i < text.length / 2; i++) { + byte[] keyword = text[2 * i].getBytes("UTF-8"); + byte[] value = text[2 * i + 1].getBytes("UTF-8"); + + ChunkStream cs = new ChunkStream("tEXt"); + try { + cs.write(keyword, 0, Math.min(keyword.length, 79)); + cs.write(0); + cs.write(value); + + cs.writeToStream(dataOutput); + } finally { + cs.close(); + } + } + } + } + + private void writeZTXT() throws IOException { + if (param.isCompressedTextSet()) { + String[] text = param.getCompressedText(); + + for (int i = 0; i < text.length / 2; i++) { + byte[] keyword = text[2 * i].getBytes("UTF-8"); + byte[] value = text[2 * i + 1].getBytes("UTF-8"); + + ChunkStream cs = new ChunkStream("zTXt"); + try { + cs.write(keyword, 0, Math.min(keyword.length, 79)); + cs.write(0); + cs.write(0); + + DeflaterOutputStream dos = new DeflaterOutputStream(cs); + try { + dos.write(value); + dos.finish(); + } finally { + dos.close(); + } + + cs.writeToStream(dataOutput); + } finally { + cs.close(); + } + } + } + } + + private void writePrivateChunks() throws IOException { + int numChunks = param.getNumPrivateChunks(); + for (int i = 0; i < numChunks; i++) { + String type = param.getPrivateChunkType(i); + byte[] data = param.getPrivateChunkData(i); + + ChunkStream cs = new ChunkStream(type); + try { + cs.write(data); + cs.writeToStream(dataOutput); + } finally { + cs.close(); + } + } + } + + /** + * Analyzes a set of palettes and determines if it can be expressed + * as a standard set of gray values, with zero or one values being + * fully transparent and the rest being fully opaque. If it + * is possible to express the data thusly, the method returns + * a suitable instance of PNGEncodeParam.Gray; otherwise it + * returns null. + */ + private PNGEncodeParam.Gray createGrayParam(byte[] redPalette, + byte[] greenPalette, + byte[] bluePalette, + byte[] alphaPalette) { + PNGEncodeParam.Gray param = new PNGEncodeParam.Gray(); + int numTransparent = 0; + + int grayFactor = 255 / ((1 << bitDepth) - 1); + int entries = 1 << bitDepth; + for (int i = 0; i < entries; i++) { + byte red = redPalette[i]; + if ((red != i * grayFactor) + || (red != greenPalette[i]) + || (red != bluePalette[i])) { + return null; + } + + // All alphas must be 255 except at most 1 can be 0 + byte alpha = alphaPalette[i]; + if (alpha == (byte)0) { + param.setTransparentGray(i); + + ++numTransparent; + if (numTransparent > 1) { + return null; + } + } else if (alpha != (byte)255) { + return null; + } + } + + return param; + } + + /** + * This method encodes a RenderedImage into PNG. + * The stream into which the PNG is dumped is not closed at + * the end of the operation, this should be done if needed + * by the caller of this method. + */ + @Override + public void encode(RenderedImage im) throws IOException { + this.image = im; + this.width = image.getWidth(); + this.height = image.getHeight(); + + SampleModel sampleModel = image.getSampleModel(); + + int[] sampleSize = sampleModel.getSampleSize(); + + // Set bitDepth to a sentinel value + this.bitDepth = -1; + this.bitShift = 0; + + // Allow user to override the bit depth of gray images + if (param instanceof PNGEncodeParam.Gray) { + PNGEncodeParam.Gray paramg = (PNGEncodeParam.Gray)param; + if (paramg.isBitDepthSet()) { + this.bitDepth = paramg.getBitDepth(); + } + + if (paramg.isBitShiftSet()) { + this.bitShift = paramg.getBitShift(); + } + } + + // Get bit depth from image if not set in param + if (this.bitDepth == -1) { + // Get bit depth from channel 0 of the image + + this.bitDepth = sampleSize[0]; + // Ensure all channels have the same bit depth + for (int i = 1; i < sampleSize.length; i++) { + if (sampleSize[i] != bitDepth) { + throw new RuntimeException(PropertyUtil.getString("PNGImageEncoder0")); + } + } + + // Round bit depth up to a power of 2 + if (bitDepth > 2 && bitDepth < 4) { + bitDepth = 4; + } else if (bitDepth > 4 && bitDepth < 8) { + bitDepth = 8; + } else if (bitDepth > 8 && bitDepth < 16) { + bitDepth = 16; + } else if (bitDepth > 16) { + throw new RuntimeException(PropertyUtil.getString("PNGImageEncoder1")); + } + } + + this.numBands = sampleModel.getNumBands(); + this.bpp = numBands * ((bitDepth == 16) ? 2 : 1); + + ColorModel colorModel = image.getColorModel(); + if (colorModel instanceof IndexColorModel) { + if (bitDepth < 1 || bitDepth > 8) { + throw new RuntimeException(PropertyUtil.getString("PNGImageEncoder2")); + } + if (sampleModel.getNumBands() != 1) { + throw new RuntimeException(PropertyUtil.getString("PNGImageEncoder3")); + } + + IndexColorModel icm = (IndexColorModel)colorModel; + int size = icm.getMapSize(); + + redPalette = new byte[size]; + greenPalette = new byte[size]; + bluePalette = new byte[size]; + alphaPalette = new byte[size]; + + icm.getReds(redPalette); + icm.getGreens(greenPalette); + icm.getBlues(bluePalette); + icm.getAlphas(alphaPalette); + + this.bpp = 1; + + if (param == null) { + param = createGrayParam(redPalette, + greenPalette, + bluePalette, + alphaPalette); + } + + // If param is still null, it can't be expressed as gray + if (param == null) { + param = new PNGEncodeParam.Palette(); + } + + if (param instanceof PNGEncodeParam.Palette) { + // If palette not set in param, create one from the ColorModel. + PNGEncodeParam.Palette parami = (PNGEncodeParam.Palette)param; + if (parami.isPaletteSet()) { + int[] palette = parami.getPalette(); + size = palette.length / 3; + + int index = 0; + for (int i = 0; i < size; i++) { + redPalette[i] = (byte)palette[index++]; + greenPalette[i] = (byte)palette[index++]; + bluePalette[i] = (byte)palette[index++]; + alphaPalette[i] = (byte)255; + } + } + this.colorType = PNG_COLOR_PALETTE; + } else if (param instanceof PNGEncodeParam.Gray) { + redPalette = greenPalette = bluePalette = alphaPalette = null; + this.colorType = PNG_COLOR_GRAY; + } else { + throw new RuntimeException(PropertyUtil.getString("PNGImageEncoder4")); + } + } else if (numBands == 1) { + if (param == null) { + param = new PNGEncodeParam.Gray(); + } + this.colorType = PNG_COLOR_GRAY; + } else if (numBands == 2) { + if (param == null) { + param = new PNGEncodeParam.Gray(); + } + + if (param.isTransparencySet()) { + skipAlpha = true; + numBands = 1; + if ((sampleSize[0] == 8) && (bitDepth < 8)) { + compressGray = true; + } + bpp = (bitDepth == 16) ? 2 : 1; + this.colorType = PNG_COLOR_GRAY; + } else { + if (this.bitDepth < 8) { + this.bitDepth = 8; + } + this.colorType = PNG_COLOR_GRAY_ALPHA; + } + } else if (numBands == 3) { + if (param == null) { + param = new PNGEncodeParam.RGB(); + } + this.colorType = PNG_COLOR_RGB; + } else if (numBands == 4) { + if (param == null) { + param = new PNGEncodeParam.RGB(); + } + if (param.isTransparencySet()) { + skipAlpha = true; + numBands = 3; + bpp = (bitDepth == 16) ? 6 : 3; + this.colorType = PNG_COLOR_RGB; + } else { + this.colorType = PNG_COLOR_RGB_ALPHA; + } + } + + interlace = param.getInterlacing(); + + writeMagic(); + + writeIHDR(); + + writeCHRM(); + writeGAMA(); + writeICCP(); + writeSBIT(); + writeSRGB(); + + writePLTE(); + + writeHIST(); + writeTRNS(); + writeBKGD(); + + writePHYS(); + writeSPLT(); + writeTIME(); + writeTEXT(); + writeZTXT(); + + writePrivateChunks(); + + writeIDAT(); + + writeIEND(); + + dataOutput.flush(); + dataOutput.close(); + } +} diff --git a/src/main/java/org/apache/xmlgraphics/image/codec/png/PNGRed.java b/src/main/java/org/apache/xmlgraphics/image/codec/png/PNGRed.java new file mode 100644 index 0000000..602da7b --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/codec/png/PNGRed.java @@ -0,0 +1,1863 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PNGRed.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.image.codec.png; + +import java.awt.Color; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.Transparency; +import java.awt.color.ColorSpace; +import java.awt.image.ColorModel; +import java.awt.image.ComponentColorModel; +import java.awt.image.DataBuffer; +import java.awt.image.DataBufferByte; +import java.awt.image.DataBufferUShort; +import java.awt.image.IndexColorModel; +import java.awt.image.Raster; +import java.awt.image.SampleModel; +import java.awt.image.WritableRaster; +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.SequenceInputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.TimeZone; +import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; + +import org.apache.xmlgraphics.image.GraphicsUtil; +import org.apache.xmlgraphics.image.codec.util.PropertyUtil; +import org.apache.xmlgraphics.image.rendered.AbstractRed; +import org.apache.xmlgraphics.image.rendered.CachableRed; + +// CSOFF: ConstantName +// CSOFF: InnerAssignment +// CSOFF: MethodName +// CSOFF: MissingSwitchDefault +// CSOFF: MultipleVariableDeclarations +// CSOFF: NoWhitespaceAfter +// CSOFF: OperatorWrap +// CSOFF: WhitespaceAround + +/** + * @version $Id: PNGRed.java 1732018 2016-02-24 04:51:06Z gadams $ + */ +public class PNGRed extends AbstractRed { + + static class PNGChunk { + int length; + int type; + byte[] data; + + String typeString; + + public PNGChunk(int length, int type, byte[] data, int crc) { + this.length = length; + this.type = type; + this.data = data; + + typeString = ""; + typeString += (char)(type >> 24); + typeString += (char)((type >> 16) & 0xff); + typeString += (char)((type >> 8) & 0xff); + typeString += (char)(type & 0xff); + } + + public int getLength() { + return length; + } + + public int getType() { + return type; + } + + public String getTypeString() { + return typeString; + } + + public byte[] getData() { + return data; + } + + public byte getByte(int offset) { + return data[offset]; + } + + public int getInt1(int offset) { + return data[offset] & 0xff; + } + + public int getInt2(int offset) { + return ((data[offset] & 0xff) << 8) + | (data[offset + 1] & 0xff); + } + + public int getInt4(int offset) { + return ((data[offset] & 0xff) << 24) + | ((data[offset + 1] & 0xff) << 16) + | ((data[offset + 2] & 0xff) << 8) + | (data[offset + 3] & 0xff); + } + + public String getString4(int offset) { + String s = ""; + s += (char)data[offset]; + s += (char)data[offset + 1]; + s += (char)data[offset + 2]; + s += (char)data[offset + 3]; + return s; + } + + public boolean isType(String typeName) { + return typeString.equals(typeName); + } + } + + public static final int PNG_COLOR_GRAY = 0; + public static final int PNG_COLOR_RGB = 2; + public static final int PNG_COLOR_PALETTE = 3; + public static final int PNG_COLOR_GRAY_ALPHA = 4; + public static final int PNG_COLOR_RGB_ALPHA = 6; + + private static final String[] colorTypeNames = { + "Grayscale", "Error", "Truecolor", "Index", + "Grayscale with alpha", "Error", "Truecolor with alpha" + }; + + public static final int PNG_FILTER_NONE = 0; + public static final int PNG_FILTER_SUB = 1; + public static final int PNG_FILTER_UP = 2; + public static final int PNG_FILTER_AVERAGE = 3; + public static final int PNG_FILTER_PAETH = 4; + + private int[][] bandOffsets = { + null, + { 0 }, // G + { 0, 1 }, // GA in GA order + { 0, 1, 2 }, // RGB in RGB order + { 0, 1, 2, 3 } // RGBA in RGBA order + }; + + private int bitDepth; + private int colorType; + + private int compressionMethod; + private int filterMethod; + private int interlaceMethod; + + private int paletteEntries; + private byte[] redPalette; + private byte[] greenPalette; + private byte[] bluePalette; + private byte[] alphaPalette; + + private int bkgdRed; + private int bkgdGreen; + private int bkgdBlue; + + private int grayTransparentAlpha; + private int redTransparentAlpha; + private int greenTransparentAlpha; + private int blueTransparentAlpha; + + private int maxOpacity; + + private int[] significantBits; + + // Parameter information + + // If true, the user wants destination alpha where applicable. + private boolean suppressAlpha; + + // If true, perform palette lookup internally + private boolean expandPalette; + + // If true, output < 8 bit gray images in 8 bit components format + private boolean output8BitGray; + + // Create an alpha channel in the destination color model. + private boolean outputHasAlphaPalette; + + // Perform gamma correction on the image + private boolean performGammaCorrection; + + // Expand GA to GGGA for compatbility with Java2D + private boolean expandGrayAlpha; + + // Produce an instance of PNGEncodeParam + private boolean generateEncodeParam; + + // PNGDecodeParam controlling decode process + private PNGDecodeParam decodeParam; + + // PNGEncodeParam to store file details in + private PNGEncodeParam encodeParam; + + private boolean emitProperties = true; + + private float fileGamma = 45455 / 100000.0F; + + private float userExponent = 1.0F; + + private float displayExponent = 2.2F; + + private float[] chromaticity; + + private int sRGBRenderingIntent = -1; + + // Post-processing step implied by above parameters + private int postProcess = POST_NONE; + + // Possible post-processing steps + + // Do nothing + private static final int POST_NONE = 0; + + // Gamma correct only + private static final int POST_GAMMA = 1; + + // Push gray values through grayLut to expand to 8 bits + private static final int POST_GRAY_LUT = 2; + + // Push gray values through grayLut to expand to 8 bits, add alpha + private static final int POST_GRAY_LUT_ADD_TRANS = 3; + + // Push palette value through R,G,B lookup tables + private static final int POST_PALETTE_TO_RGB = 4; + + // Push palette value through R,G,B,A lookup tables + private static final int POST_PALETTE_TO_RGBA = 5; + + // Add transparency to a given gray value (w/ optional gamma) + private static final int POST_ADD_GRAY_TRANS = 6; + + // Add transparency to a given RGB value (w/ optional gamma) + private static final int POST_ADD_RGB_TRANS = 7; + + // Remove the alpha channel from a gray image (w/ optional gamma) + private static final int POST_REMOVE_GRAY_TRANS = 8; + + // Remove the alpha channel from an RGB image (w/optional gamma) + private static final int POST_REMOVE_RGB_TRANS = 9; + + // Mask to add expansion of GA -> GGGA + private static final int POST_EXP_MASK = 16; + + // Expand gray to G/G/G + private static final int POST_GRAY_ALPHA_EXP = + POST_NONE | POST_EXP_MASK; + + // Expand gray to G/G/G through a gamma lut + private static final int POST_GAMMA_EXP = + POST_GAMMA | POST_EXP_MASK; + + // Push gray values through grayLut to expand to 8 bits, expand, add alpha + private static final int POST_GRAY_LUT_ADD_TRANS_EXP = + POST_GRAY_LUT_ADD_TRANS | POST_EXP_MASK; + + // Add transparency to a given gray value, expand + private static final int POST_ADD_GRAY_TRANS_EXP = + POST_ADD_GRAY_TRANS | POST_EXP_MASK; + + private List streamVec = new ArrayList(); + private DataInputStream dataStream; + + private int bytesPerPixel; // number of bytes per input pixel + private int inputBands; + private int outputBands; + + // Number of private chunks + private int chunkIndex; + + private List textKeys = new ArrayList(); + private List textStrings = new ArrayList(); + + private List ztextKeys = new ArrayList(); + private List ztextStrings = new ArrayList(); + + private WritableRaster theTile; + private Rectangle bounds; + + /** A Hashtable containing the image properties. */ + private Map properties = new HashMap(); + + + private int[] gammaLut; + + private void initGammaLut(int bits) { + double exp = (double)userExponent / (fileGamma * displayExponent); + int numSamples = 1 << bits; + int maxOutSample = (bits == 16) ? 65535 : 255; + + gammaLut = new int[numSamples]; + for (int i = 0; i < numSamples; i++) { + double gbright = (double)i / (numSamples - 1); + double gamma = Math.pow(gbright, exp); + int igamma = (int)(gamma * maxOutSample + 0.5); + if (igamma > maxOutSample) { + igamma = maxOutSample; + } + gammaLut[i] = igamma; + } + } + + private final byte[][] expandBits = { + null, + { (byte)0x00, (byte)0xff }, + { (byte)0x00, (byte)0x55, (byte)0xaa, (byte)0xff }, + null, + { (byte)0x00, (byte)0x11, (byte)0x22, (byte)0x33, + (byte)0x44, (byte)0x55, (byte)0x66, (byte)0x77, + (byte)0x88, (byte)0x99, (byte)0xaa, (byte)0xbb, + (byte)0xcc, (byte)0xdd, (byte)0xee, (byte)0xff } + }; + + private int[] grayLut; + + private void initGrayLut(int bits) { + int len = 1 << bits; + grayLut = new int[len]; + + if (performGammaCorrection) { + System.arraycopy(gammaLut, 0, grayLut, 0, len); + } else { + for (int i = 0; i < len; i++) { + grayLut[i] = expandBits[bits][i]; + } + } + } + + public PNGRed(InputStream stream) throws IOException { + this(stream, null); + } + + public PNGRed(InputStream stream, PNGDecodeParam decodeParam) + throws IOException { + + if (!stream.markSupported()) { + stream = new BufferedInputStream(stream); + } + DataInputStream distream = new DataInputStream(stream); + + if (decodeParam == null) { + decodeParam = new PNGDecodeParam(); + } + this.decodeParam = decodeParam; + + // Get parameter values + this.suppressAlpha = decodeParam.getSuppressAlpha(); + this.expandPalette = decodeParam.getExpandPalette(); + this.output8BitGray = decodeParam.getOutput8BitGray(); + this.expandGrayAlpha = decodeParam.getExpandGrayAlpha(); + if (decodeParam.getPerformGammaCorrection()) { + this.userExponent = decodeParam.getUserExponent(); + this.displayExponent = decodeParam.getDisplayExponent(); + performGammaCorrection = true; + output8BitGray = true; + } + this.generateEncodeParam = decodeParam.getGenerateEncodeParam(); + + if (emitProperties) { + properties.put("file_type", "PNG v. 1.0"); + } + + long magic = distream.readLong(); + if (magic != 0x89504e470d0a1a0aL) { + String msg = PropertyUtil.getString("PNGImageDecoder0"); + throw new RuntimeException(msg); + } + + do { + PNGChunk chunk; + + String chunkType = getChunkType(distream); + if (chunkType.equals("IHDR")) { + chunk = readChunk(distream); + parse_IHDR_chunk(chunk); + } else if (chunkType.equals("PLTE")) { + chunk = readChunk(distream); + parse_PLTE_chunk(chunk); + } else if (chunkType.equals("IDAT")) { + chunk = readChunk(distream); + streamVec.add(new ByteArrayInputStream(chunk.getData())); + } else if (chunkType.equals("IEND")) { + chunk = readChunk(distream); + try { + parse_IEND_chunk(chunk); + } catch (Exception e) { + e.printStackTrace(); + String msg = PropertyUtil.getString("PNGImageDecoder2"); + throw new RuntimeException(msg); + } + break; // fall through to the bottom + } else if (chunkType.equals("bKGD")) { + chunk = readChunk(distream); + parse_bKGD_chunk(chunk); + } else if (chunkType.equals("cHRM")) { + chunk = readChunk(distream); + parse_cHRM_chunk(chunk); + } else if (chunkType.equals("gAMA")) { + chunk = readChunk(distream); + parse_gAMA_chunk(chunk); + } else if (chunkType.equals("hIST")) { + chunk = readChunk(distream); + parse_hIST_chunk(chunk); + } else if (chunkType.equals("iCCP")) { + chunk = readChunk(distream); + } else if (chunkType.equals("pHYs")) { + chunk = readChunk(distream); + parse_pHYs_chunk(chunk); + } else if (chunkType.equals("sBIT")) { + chunk = readChunk(distream); + parse_sBIT_chunk(chunk); + } else if (chunkType.equals("sRGB")) { + chunk = readChunk(distream); + parse_sRGB_chunk(chunk); + } else if (chunkType.equals("tEXt")) { + chunk = readChunk(distream); + parse_tEXt_chunk(chunk); + } else if (chunkType.equals("tIME")) { + chunk = readChunk(distream); + parse_tIME_chunk(chunk); + } else if (chunkType.equals("tRNS")) { + chunk = readChunk(distream); + parse_tRNS_chunk(chunk); + } else if (chunkType.equals("zTXt")) { + chunk = readChunk(distream); + parse_zTXt_chunk(chunk); + } else { + chunk = readChunk(distream); + // Output the chunk data in raw form + + String type = chunk.getTypeString(); + byte[] data = chunk.getData(); + if (encodeParam != null) { + encodeParam.addPrivateChunk(type, data); + } + if (emitProperties) { + String key = "chunk_" + chunkIndex++ + ':' + type; + properties.put(key.toLowerCase(Locale.getDefault()), data); + } + } + } while (true); + + // Final post-processing + + if (significantBits == null) { + significantBits = new int[inputBands]; + for (int i = 0; i < inputBands; i++) { + significantBits[i] = bitDepth; + } + + if (emitProperties) { + properties.put("significant_bits", significantBits); + } + } + distream.close(); + stream.close(); + } + + private static String getChunkType(DataInputStream distream) { + try { + distream.mark(8); + /* int length = */ distream.readInt(); + int type = distream.readInt(); + distream.reset(); + + String typeString = "" + + (char)((type >> 24) & 0xff) + + (char)((type >> 16) & 0xff) + + (char)((type >> 8) & 0xff) + + (char)(type & 0xff); + return typeString; + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + private static PNGChunk readChunk(DataInputStream distream) { + try { + int length = distream.readInt(); + int type = distream.readInt(); + byte[] data = new byte[length]; + distream.readFully(data); + int crc = distream.readInt(); + + return new PNGChunk(length, type, data, crc); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + private void parse_IHDR_chunk(PNGChunk chunk) { + int width = chunk.getInt4(0); + int height = chunk.getInt4(4); + + bounds = new Rectangle(0, 0, width, height); + + bitDepth = chunk.getInt1(8); + + int validMask = (1 << 1) | (1 << 2) | (1 << 4) | (1 << 8) | (1 << 16); + if (((1 << bitDepth) & validMask) == 0) { + // bitDepth is not one of { 1, 2, 4, 8, 16 }: Error -- bad bit depth + String msg = PropertyUtil.getString("PNGImageDecoder3"); + throw new RuntimeException(msg); + } + maxOpacity = (1 << bitDepth) - 1; + + colorType = chunk.getInt1(9); + if ((colorType != PNG_COLOR_GRAY) + && (colorType != PNG_COLOR_RGB) + && (colorType != PNG_COLOR_PALETTE) + && (colorType != PNG_COLOR_GRAY_ALPHA) + && (colorType != PNG_COLOR_RGB_ALPHA)) { + System.out.println(PropertyUtil.getString("PNGImageDecoder4")); + } + + if ((colorType == PNG_COLOR_RGB) && (bitDepth < 8)) { + // Error -- RGB images must have 8 or 16 bits + String msg = PropertyUtil.getString("PNGImageDecoder5"); + throw new RuntimeException(msg); + } + + if ((colorType == PNG_COLOR_PALETTE) && (bitDepth == 16)) { + // Error -- palette images must have < 16 bits + String msg = PropertyUtil.getString("PNGImageDecoder6"); + throw new RuntimeException(msg); + } + + if ((colorType == PNG_COLOR_GRAY_ALPHA) && (bitDepth < 8)) { + // Error -- gray/alpha images must have >= 8 bits + String msg = PropertyUtil.getString("PNGImageDecoder7"); + throw new RuntimeException(msg); + } + + if ((colorType == PNG_COLOR_RGB_ALPHA) && (bitDepth < 8)) { + // Error -- RGB/alpha images must have >= 8 bits + String msg = PropertyUtil.getString("PNGImageDecoder8"); + throw new RuntimeException(msg); + } + + if (emitProperties) { + properties.put("color_type", colorTypeNames[colorType]); + } + + if (generateEncodeParam) { + if (colorType == PNG_COLOR_PALETTE) { + encodeParam = new PNGEncodeParam.Palette(); + } else if (colorType == PNG_COLOR_GRAY + || colorType == PNG_COLOR_GRAY_ALPHA) { + encodeParam = new PNGEncodeParam.Gray(); + } else { + encodeParam = new PNGEncodeParam.RGB(); + } + decodeParam.setEncodeParam(encodeParam); + } + + if (encodeParam != null) { + encodeParam.setBitDepth(bitDepth); + } + if (emitProperties) { + properties.put("bit_depth", bitDepth); + } + + if (performGammaCorrection) { + // Assume file gamma is 1/2.2 unless we get a gAMA chunk + float gamma = (1.0F / 2.2F) * (displayExponent / userExponent); + if (encodeParam != null) { + encodeParam.setGamma(gamma); + } + if (emitProperties) { + properties.put("gamma", gamma); + } + } + + compressionMethod = chunk.getInt1(10); + if (compressionMethod != 0) { + // Error -- only know about compression method 0 + String msg = PropertyUtil.getString("PNGImageDecoder9"); + throw new RuntimeException(msg); + } + + filterMethod = chunk.getInt1(11); + if (filterMethod != 0) { + // Error -- only know about filter method 0 + String msg = PropertyUtil.getString("PNGImageDecoder10"); + throw new RuntimeException(msg); + } + + interlaceMethod = chunk.getInt1(12); + if (interlaceMethod == 0) { + if (encodeParam != null) { + encodeParam.setInterlacing(false); + } + if (emitProperties) { + properties.put("interlace_method", "None"); + } + } else if (interlaceMethod == 1) { + if (encodeParam != null) { + encodeParam.setInterlacing(true); + } + if (emitProperties) { + properties.put("interlace_method", "Adam7"); + } + } else { + // Error -- only know about Adam7 interlacing + String msg = PropertyUtil.getString("PNGImageDecoder11"); + throw new RuntimeException(msg); + } + + bytesPerPixel = (bitDepth == 16) ? 2 : 1; + + switch (colorType) { + case PNG_COLOR_GRAY: + inputBands = 1; + outputBands = 1; + + if (output8BitGray && (bitDepth < 8)) { + postProcess = POST_GRAY_LUT; + } else if (performGammaCorrection) { + postProcess = POST_GAMMA; + } else { + postProcess = POST_NONE; + } + break; + + case PNG_COLOR_RGB: + inputBands = 3; + bytesPerPixel *= 3; + outputBands = 3; + + if (performGammaCorrection) { + postProcess = POST_GAMMA; + } else { + postProcess = POST_NONE; + } + break; + + case PNG_COLOR_PALETTE: + inputBands = 1; + bytesPerPixel = 1; + outputBands = expandPalette ? 3 : 1; + + if (expandPalette) { + postProcess = POST_PALETTE_TO_RGB; + } else { + postProcess = POST_NONE; + } + break; + + case PNG_COLOR_GRAY_ALPHA: + inputBands = 2; + bytesPerPixel *= 2; + + if (suppressAlpha) { + outputBands = 1; + postProcess = POST_REMOVE_GRAY_TRANS; + } else { + if (performGammaCorrection) { + postProcess = POST_GAMMA; + } else { + postProcess = POST_NONE; + } + if (expandGrayAlpha) { + postProcess |= POST_EXP_MASK; + outputBands = 4; + } else { + outputBands = 2; + } + } + break; + + case PNG_COLOR_RGB_ALPHA: + inputBands = 4; + bytesPerPixel *= 4; + outputBands = (!suppressAlpha) ? 4 : 3; + + if (suppressAlpha) { + postProcess = POST_REMOVE_RGB_TRANS; + } else if (performGammaCorrection) { + postProcess = POST_GAMMA; + } else { + postProcess = POST_NONE; + } + break; + } + } + + private void parse_IEND_chunk(PNGChunk chunk) throws Exception { + // Store text strings + int textLen = textKeys.size(); + String[] textArray = new String[2 * textLen]; + for (int i = 0; i < textLen; i++) { + String key = (String)textKeys.get(i); + String val = (String)textStrings.get(i); + textArray[2 * i] = key; + textArray[2 * i + 1] = val; + if (emitProperties) { + String uniqueKey = "text_" + i + ':' + key; + properties.put(uniqueKey.toLowerCase(Locale.getDefault()), val); + } + } + if (encodeParam != null) { + encodeParam.setText(textArray); + } + + // Store compressed text strings + int ztextLen = ztextKeys.size(); + String[] ztextArray = new String[2 * ztextLen]; + for (int i = 0; i < ztextLen; i++) { + String key = (String)ztextKeys.get(i); + String val = (String)ztextStrings.get(i); + ztextArray[2 * i] = key; + ztextArray[2 * i + 1] = val; + if (emitProperties) { + String uniqueKey = "ztext_" + i + ':' + key; + properties.put(uniqueKey.toLowerCase(Locale.getDefault()), val); + } + } + if (encodeParam != null) { + encodeParam.setCompressedText(ztextArray); + } + + // Parse prior IDAT chunks + InputStream seqStream = + new SequenceInputStream(Collections.enumeration(streamVec)); + InputStream infStream = + new InflaterInputStream(seqStream, new Inflater()); + dataStream = new DataInputStream(infStream); + + // Create an empty WritableRaster + int depth = bitDepth; + if ((colorType == PNG_COLOR_GRAY) + && (bitDepth < 8) && output8BitGray) { + depth = 8; + } + if ((colorType == PNG_COLOR_PALETTE) && expandPalette) { + depth = 8; + } + int width = bounds.width; + int height = bounds.height; + + int bytesPerRow = (outputBands * width * depth + 7) / 8; + int scanlineStride = + (depth == 16) ? (bytesPerRow / 2) : bytesPerRow; + + theTile = createRaster(width, height, outputBands, + scanlineStride, + depth); + + if (performGammaCorrection && (gammaLut == null)) { + initGammaLut(bitDepth); + } + if ((postProcess == POST_GRAY_LUT) + || (postProcess == POST_GRAY_LUT_ADD_TRANS) + || (postProcess == POST_GRAY_LUT_ADD_TRANS_EXP)) { + initGrayLut(bitDepth); + } + + decodeImage(interlaceMethod == 1); + + // Free resources associated with compressed data. + dataStream.close(); + infStream.close(); + seqStream.close(); + streamVec = null; + + SampleModel sm = theTile.getSampleModel(); + ColorModel cm; + + if ((colorType == PNG_COLOR_PALETTE) && !expandPalette) { + if (outputHasAlphaPalette) { + cm = new IndexColorModel(bitDepth, + paletteEntries, + redPalette, + greenPalette, + bluePalette, + alphaPalette); + } else { + cm = new IndexColorModel(bitDepth, + paletteEntries, + redPalette, + greenPalette, + bluePalette); + } + } else if ((colorType == PNG_COLOR_GRAY) + && (bitDepth < 8) && !output8BitGray) { + byte[] palette = expandBits[bitDepth]; + cm = new IndexColorModel(bitDepth, + palette.length, + palette, + palette, + palette); + } else { + cm = + createComponentColorModel(sm); + } + + init((CachableRed)null, bounds, cm, sm, 0, 0, properties); + } + + private static final int[] GrayBits8 = { 8 }; + private static final ComponentColorModel colorModelGray8 = + new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_GRAY), + GrayBits8, false, false, + Transparency.OPAQUE, + DataBuffer.TYPE_BYTE); + + private static final int[] GrayAlphaBits8 = { 8, 8 }; + private static final ComponentColorModel colorModelGrayAlpha8 = + new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_GRAY), + GrayAlphaBits8, true, false, + Transparency.TRANSLUCENT, + DataBuffer.TYPE_BYTE); + + private static final int[] GrayBits16 = { 16 }; + private static final ComponentColorModel colorModelGray16 = + new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_GRAY), + GrayBits16, false, false, + Transparency.OPAQUE, + DataBuffer.TYPE_USHORT); + + private static final int[] GrayAlphaBits16 = { 16, 16 }; + private static final ComponentColorModel colorModelGrayAlpha16 = + new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_GRAY), + GrayAlphaBits16, true, false, + Transparency.TRANSLUCENT, + DataBuffer.TYPE_USHORT); + + private static final int[] GrayBits32 = { 32 }; + private static final ComponentColorModel colorModelGray32 = + new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_GRAY), + GrayBits32, false, false, + Transparency.OPAQUE, + DataBuffer.TYPE_INT); + + private static final int[] GrayAlphaBits32 = { 32, 32 }; + private static final ComponentColorModel colorModelGrayAlpha32 = + new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_GRAY), + GrayAlphaBits32, true, false, + Transparency.TRANSLUCENT, + DataBuffer.TYPE_INT); + + private static final int[] RGBBits8 = { 8, 8, 8 }; + private static final ComponentColorModel colorModelRGB8 = + new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), + RGBBits8, false, false, + Transparency.OPAQUE, + DataBuffer.TYPE_BYTE); + + private static final int[] RGBABits8 = { 8, 8, 8, 8 }; + private static final ComponentColorModel colorModelRGBA8 = + new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), + RGBABits8, true, false, + Transparency.TRANSLUCENT, + DataBuffer.TYPE_BYTE); + + private static final int[] RGBBits16 = { 16, 16, 16 }; + private static final ComponentColorModel colorModelRGB16 = + new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), + RGBBits16, false, false, + Transparency.OPAQUE, + DataBuffer.TYPE_USHORT); + + private static final int[] RGBABits16 = { 16, 16, 16, 16 }; + private static final ComponentColorModel colorModelRGBA16 = + new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), + RGBABits16, true, false, + Transparency.TRANSLUCENT, + DataBuffer.TYPE_USHORT); + + private static final int[] RGBBits32 = { 32, 32, 32 }; + private static final ComponentColorModel colorModelRGB32 = + new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), + RGBBits32, false, false, + Transparency.OPAQUE, + DataBuffer.TYPE_INT); + + private static final int[] RGBABits32 = { 32, 32, 32, 32 }; + private static final ComponentColorModel colorModelRGBA32 = + new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), + RGBABits32, true, false, + Transparency.TRANSLUCENT, + DataBuffer.TYPE_INT); + /** + * A convenience method to create an instance of + * ComponentColorModel suitable for use with the + * given SampleModel. The SampleModel + * should have a data type of DataBuffer.TYPE_BYTE, + * TYPE_USHORT, or TYPE_INT and between + * 1 and 4 bands. Depending on the number of bands of the + * SampleModel, either a gray, gray+alpha, rgb, or + * rgb+alpha ColorModel is returned. + */ + public static ColorModel createComponentColorModel(SampleModel sm) { + int type = sm.getDataType(); + int bands = sm.getNumBands(); + ComponentColorModel cm = null; + + if (type == DataBuffer.TYPE_BYTE) { + switch (bands) { + case 1: + cm = colorModelGray8; + break; + case 2: + cm = colorModelGrayAlpha8; + break; + case 3: + cm = colorModelRGB8; + break; + case 4: + cm = colorModelRGBA8; + break; + } + } else if (type == DataBuffer.TYPE_USHORT) { + switch (bands) { + case 1: + cm = colorModelGray16; + break; + case 2: + cm = colorModelGrayAlpha16; + break; + case 3: + cm = colorModelRGB16; + break; + case 4: + cm = colorModelRGBA16; + break; + } + } else if (type == DataBuffer.TYPE_INT) { + switch (bands) { + case 1: + cm = colorModelGray32; + break; + case 2: + cm = colorModelGrayAlpha32; + break; + case 3: + cm = colorModelRGB32; + break; + case 4: + cm = colorModelRGBA32; + break; + } + } + + return cm; + } + + private void parse_PLTE_chunk(PNGChunk chunk) { + paletteEntries = chunk.getLength() / 3; + redPalette = new byte[paletteEntries]; + greenPalette = new byte[paletteEntries]; + bluePalette = new byte[paletteEntries]; + + int pltIndex = 0; + + // gAMA chunk must precede PLTE chunk + if (performGammaCorrection) { + if (gammaLut == null) { + initGammaLut(bitDepth == 16 ? 16 : 8); + } + + for (int i = 0; i < paletteEntries; i++) { + byte r = chunk.getByte(pltIndex++); + byte g = chunk.getByte(pltIndex++); + byte b = chunk.getByte(pltIndex++); + + redPalette[i] = (byte)gammaLut[r & 0xff]; + greenPalette[i] = (byte)gammaLut[g & 0xff]; + bluePalette[i] = (byte)gammaLut[b & 0xff]; + } + } else { + for (int i = 0; i < paletteEntries; i++) { + redPalette[i] = chunk.getByte(pltIndex++); + greenPalette[i] = chunk.getByte(pltIndex++); + bluePalette[i] = chunk.getByte(pltIndex++); + } + } + } + + private void parse_bKGD_chunk(PNGChunk chunk) { + switch (colorType) { + case PNG_COLOR_PALETTE: + int bkgdIndex = chunk.getByte(0) & 0xff; + + bkgdRed = redPalette[bkgdIndex] & 0xff; + bkgdGreen = greenPalette[bkgdIndex] & 0xff; + bkgdBlue = bluePalette[bkgdIndex] & 0xff; + + if (encodeParam != null) { + ((PNGEncodeParam.Palette)encodeParam).setBackgroundPaletteIndex(bkgdIndex); + } + break; + case PNG_COLOR_GRAY: case PNG_COLOR_GRAY_ALPHA: + int bkgdGray = chunk.getInt2(0); + bkgdRed = bkgdGreen = bkgdBlue = bkgdGray; + + if (encodeParam != null) { + ((PNGEncodeParam.Gray)encodeParam).setBackgroundGray(bkgdGray); + } + break; + case PNG_COLOR_RGB: case PNG_COLOR_RGB_ALPHA: + bkgdRed = chunk.getInt2(0); + bkgdGreen = chunk.getInt2(2); + bkgdBlue = chunk.getInt2(4); + + int[] bkgdRGB = new int[3]; + bkgdRGB[0] = bkgdRed; + bkgdRGB[1] = bkgdGreen; + bkgdRGB[2] = bkgdBlue; + if (encodeParam != null) { + ((PNGEncodeParam.RGB)encodeParam).setBackgroundRGB(bkgdRGB); + } + break; + } + + if (emitProperties) { + int r = 0; + int g = 0; + int b = 0; + if ((colorType == PNG_COLOR_PALETTE) || (bitDepth == 8)) { + r = bkgdRed; + g = bkgdGreen; + b = bkgdBlue; + } else if (bitDepth < 8) { + r = expandBits[bitDepth][bkgdRed]; + g = expandBits[bitDepth][bkgdGreen]; + b = expandBits[bitDepth][bkgdBlue]; + } else if (bitDepth == 16) { + r = bkgdRed >> 8; + g = bkgdGreen >> 8; + b = bkgdBlue >> 8; + } + properties.put("background_color", new Color(r, g, b)); + } + } + + private void parse_cHRM_chunk(PNGChunk chunk) { + // If an sRGB chunk exists, ignore cHRM chunks + if (sRGBRenderingIntent != -1) { + return; + } + + chromaticity = new float[8]; + chromaticity[0] = chunk.getInt4(0) / 100000.0F; + chromaticity[1] = chunk.getInt4(4) / 100000.0F; + chromaticity[2] = chunk.getInt4(8) / 100000.0F; + chromaticity[3] = chunk.getInt4(12) / 100000.0F; + chromaticity[4] = chunk.getInt4(16) / 100000.0F; + chromaticity[5] = chunk.getInt4(20) / 100000.0F; + chromaticity[6] = chunk.getInt4(24) / 100000.0F; + chromaticity[7] = chunk.getInt4(28) / 100000.0F; + + if (encodeParam != null) { + encodeParam.setChromaticity(chromaticity); + } + if (emitProperties) { + properties.put("white_point_x", chromaticity[0]); + properties.put("white_point_y", chromaticity[1]); + properties.put("red_x", chromaticity[2]); + properties.put("red_y", chromaticity[3]); + properties.put("green_x", chromaticity[4]); + properties.put("green_y", chromaticity[5]); + properties.put("blue_x", chromaticity[6]); + properties.put("blue_y", chromaticity[7]); + } + } + + private void parse_gAMA_chunk(PNGChunk chunk) { + // If an sRGB chunk exists, ignore gAMA chunks + if (sRGBRenderingIntent != -1) { + return; + } + + fileGamma = chunk.getInt4(0) / 100000.0F; + // System.out.println("Gamma: " + fileGamma); + float exp = + performGammaCorrection ? displayExponent / userExponent : 1.0F; + if (encodeParam != null) { + encodeParam.setGamma(fileGamma * exp); + } + if (emitProperties) { + properties.put("gamma", fileGamma * exp); + } + } + + private void parse_hIST_chunk(PNGChunk chunk) { + if (redPalette == null) { + String msg = PropertyUtil.getString("PNGImageDecoder18"); + throw new RuntimeException(msg); + } + + int length = redPalette.length; + int[] hist = new int[length]; + for (int i = 0; i < length; i++) { + hist[i] = chunk.getInt2(2 * i); + } + + if (encodeParam != null) { + encodeParam.setPaletteHistogram(hist); + } + } + + private void parse_pHYs_chunk(PNGChunk chunk) { + int xPixelsPerUnit = chunk.getInt4(0); + int yPixelsPerUnit = chunk.getInt4(4); + int unitSpecifier = chunk.getInt1(8); + + if (encodeParam != null) { + encodeParam.setPhysicalDimension(xPixelsPerUnit, + yPixelsPerUnit, + unitSpecifier); + } + if (emitProperties) { + properties.put("x_pixels_per_unit", xPixelsPerUnit); + properties.put("y_pixels_per_unit", yPixelsPerUnit); + properties.put("pixel_aspect_ratio", + (float) xPixelsPerUnit / yPixelsPerUnit); + if (unitSpecifier == 1) { + properties.put("pixel_units", "Meters"); + } else if (unitSpecifier != 0) { + // Error -- unit specifier must be 0 or 1 + String msg = PropertyUtil.getString("PNGImageDecoder12"); + throw new RuntimeException(msg); + } + } + } + + private void parse_sBIT_chunk(PNGChunk chunk) { + if (colorType == PNG_COLOR_PALETTE) { + significantBits = new int[3]; + } else { + significantBits = new int[inputBands]; + } + for (int i = 0; i < significantBits.length; i++) { + int bits = chunk.getByte(i); + int depth = (colorType == PNG_COLOR_PALETTE) ? 8 : bitDepth; + if (bits <= 0 || bits > depth) { + // Error -- significant bits must be between 0 and + // image bit depth. + String msg = PropertyUtil.getString("PNGImageDecoder13"); + throw new RuntimeException(msg); + } + significantBits[i] = bits; + } + + if (encodeParam != null) { + encodeParam.setSignificantBits(significantBits); + } + if (emitProperties) { + properties.put("significant_bits", significantBits); + } + } + + private void parse_sRGB_chunk(PNGChunk chunk) { + sRGBRenderingIntent = chunk.getByte(0); + + // The presence of an sRGB chunk implies particular + // settings for gamma and chroma. + fileGamma = 45455 / 100000.0F; + + chromaticity = new float[8]; + chromaticity[0] = 31270 / 10000.0F; + chromaticity[1] = 32900 / 10000.0F; + chromaticity[2] = 64000 / 10000.0F; + chromaticity[3] = 33000 / 10000.0F; + chromaticity[4] = 30000 / 10000.0F; + chromaticity[5] = 60000 / 10000.0F; + chromaticity[6] = 15000 / 10000.0F; + chromaticity[7] = 6000 / 10000.0F; + + if (performGammaCorrection) { + // File gamma is 1/2.2 + float gamma = fileGamma * (displayExponent / userExponent); + if (encodeParam != null) { + encodeParam.setGamma(gamma); + encodeParam.setChromaticity(chromaticity); + } + if (emitProperties) { + properties.put("gamma", gamma); + properties.put("white_point_x", chromaticity[0]); + properties.put("white_point_y", chromaticity[1]); + properties.put("red_x", chromaticity[2]); + properties.put("red_y", chromaticity[3]); + properties.put("green_x", chromaticity[4]); + properties.put("green_y", chromaticity[5]); + properties.put("blue_x", chromaticity[6]); + properties.put("blue_y", chromaticity[7]); + } + } + } + + private void parse_tEXt_chunk(PNGChunk chunk) { + StringBuffer key = new StringBuffer(); + StringBuffer value = new StringBuffer(); + byte b; + + int textIndex = 0; + while ((b = chunk.getByte(textIndex++)) != 0) { + key.append((char)b); + } + + for (int i = textIndex; i < chunk.getLength(); i++) { + value.append((char)chunk.getByte(i)); + } + + textKeys.add(key.toString()); + textStrings.add(value.toString()); + } + + private void parse_tIME_chunk(PNGChunk chunk) { + int year = chunk.getInt2(0); + int month = chunk.getInt1(2) - 1; + int day = chunk.getInt1(3); + int hour = chunk.getInt1(4); + int minute = chunk.getInt1(5); + int second = chunk.getInt1(6); + + TimeZone gmt = TimeZone.getTimeZone("GMT"); + + GregorianCalendar cal = new GregorianCalendar(gmt); + cal.set(year, month, day, + hour, minute, second); + Date date = cal.getTime(); + + if (encodeParam != null) { + encodeParam.setModificationTime(date); + } + if (emitProperties) { + properties.put("timestamp", date); + } + } + + private void parse_tRNS_chunk(PNGChunk chunk) { + if (colorType == PNG_COLOR_PALETTE) { + int entries = chunk.getLength(); + if (entries > paletteEntries) { + // Error -- mustn't have more alpha than RGB palette entries + String msg = PropertyUtil.getString("PNGImageDecoder14"); + throw new RuntimeException(msg); + } + + // Load beginning of palette from the chunk + alphaPalette = new byte[paletteEntries]; + for (int i = 0; i < entries; i++) { + alphaPalette[i] = chunk.getByte(i); + } + + // Fill rest of palette with 255 + for (int i = entries; i < paletteEntries; i++) { + alphaPalette[i] = (byte)255; + } + + if (!suppressAlpha) { + if (expandPalette) { + postProcess = POST_PALETTE_TO_RGBA; + outputBands = 4; + } else { + outputHasAlphaPalette = true; + } + } + } else if (colorType == PNG_COLOR_GRAY) { + grayTransparentAlpha = chunk.getInt2(0); + + if (!suppressAlpha) { + if (bitDepth < 8) { + output8BitGray = true; + maxOpacity = 255; + postProcess = POST_GRAY_LUT_ADD_TRANS; + } else { + postProcess = POST_ADD_GRAY_TRANS; + } + + if (expandGrayAlpha) { + outputBands = 4; + postProcess |= POST_EXP_MASK; + } else { + outputBands = 2; + } + + if (encodeParam != null) { + ((PNGEncodeParam.Gray)encodeParam).setTransparentGray(grayTransparentAlpha); + } + } + } else if (colorType == PNG_COLOR_RGB) { + redTransparentAlpha = chunk.getInt2(0); + greenTransparentAlpha = chunk.getInt2(2); + blueTransparentAlpha = chunk.getInt2(4); + + if (!suppressAlpha) { + outputBands = 4; + postProcess = POST_ADD_RGB_TRANS; + + if (encodeParam != null) { + int[] rgbTrans = new int[3]; + rgbTrans[0] = redTransparentAlpha; + rgbTrans[1] = greenTransparentAlpha; + rgbTrans[2] = blueTransparentAlpha; + ((PNGEncodeParam.RGB)encodeParam).setTransparentRGB(rgbTrans); + } + } + } else if (colorType == PNG_COLOR_GRAY_ALPHA + || colorType == PNG_COLOR_RGB_ALPHA) { + // Error -- GA or RGBA image can't have a tRNS chunk. + String msg = PropertyUtil.getString("PNGImageDecoder15"); + throw new RuntimeException(msg); + } + } + + private void parse_zTXt_chunk(PNGChunk chunk) { + StringBuffer key = new StringBuffer(); + StringBuffer value = new StringBuffer(); + byte b; + + int textIndex = 0; + while ((b = chunk.getByte(textIndex++)) != 0) { + key.append((char)b); + } + + // skip method + textIndex++; + + try { + int length = chunk.getLength() - textIndex; + byte[] data = chunk.getData(); + InputStream cis = + new ByteArrayInputStream(data, textIndex, length); + InputStream iis = new InflaterInputStream(cis); + + int c; + while ((c = iis.read()) != -1) { + value.append((char)c); + } + + ztextKeys.add(key.toString()); + ztextStrings.add(value.toString()); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private WritableRaster createRaster(int width, int height, int bands, + int scanlineStride, + int bitDepth) { + + DataBuffer dataBuffer; + WritableRaster ras = null; + Point origin = new Point(0, 0); + if ((bitDepth < 8) && (bands == 1)) { + dataBuffer = new DataBufferByte(height * scanlineStride); + ras = Raster.createPackedRaster(dataBuffer, + width, height, + bitDepth, + origin); + } else if (bitDepth <= 8) { + dataBuffer = new DataBufferByte(height * scanlineStride); + ras = Raster.createInterleavedRaster(dataBuffer, + width, height, + scanlineStride, + bands, + bandOffsets[bands], + origin); + } else { + dataBuffer = new DataBufferUShort(height * scanlineStride); + ras = Raster.createInterleavedRaster(dataBuffer, + width, height, + scanlineStride, + bands, + bandOffsets[bands], + origin); + } + + return ras; + } + + // Data filtering methods + + private static void decodeSubFilter(byte[] curr, int count, int bpp) { + for (int i = bpp; i < count; i++) { + int val; + + val = curr[i] & 0xff; + val += curr[i - bpp] & 0xff; + + curr[i] = (byte)val; + } + } + + private static void decodeUpFilter(byte[] curr, byte[] prev, + int count) { + for (int i = 0; i < count; i++) { + int raw = curr[i] & 0xff; + int prior = prev[i] & 0xff; + + curr[i] = (byte)(raw + prior); + } + } + + private static void decodeAverageFilter(byte[] curr, byte[] prev, + int count, int bpp) { + for (int i = 0; i < bpp; i++) { + int raw = curr[i] & 0xff; + int priorRow = prev[i] & 0xff; + + curr[i] = (byte)(raw + priorRow / 2); + } + + for (int i = bpp; i < count; i++) { + int raw = curr[i] & 0xff; + int priorPixel = curr[i - bpp] & 0xff; + int priorRow = prev[i] & 0xff; + + curr[i] = (byte)(raw + (priorPixel + priorRow) / 2); + } + } + + private static int paethPredictor(int a, int b, int c) { + int p = a + b - c; + int pa = Math.abs(p - a); + int pb = Math.abs(p - b); + int pc = Math.abs(p - c); + + if ((pa <= pb) && (pa <= pc)) { + return a; + } else if (pb <= pc) { + return b; + } else { + return c; + } + } + + private static void decodePaethFilter(byte[] curr, byte[] prev, + int count, int bpp) { + int priorPixel; + int priorRowPixel; + + for (int i = 0; i < bpp; i++) { + int raw = curr[i] & 0xff; + int priorRow = prev[i] & 0xff; + + curr[i] = (byte)(raw + priorRow); + } + + for (int i = bpp; i < count; i++) { + int raw = curr[i] & 0xff; + priorPixel = curr[i - bpp] & 0xff; + int priorRow = prev[i] & 0xff; + priorRowPixel = prev[i - bpp] & 0xff; + + curr[i] = (byte)(raw + paethPredictor(priorPixel, + priorRow, + priorRowPixel)); + } + } + + private void processPixels(int process, + Raster src, WritableRaster dst, + int xOffset, int step, int y, int width) { + int srcX; + int dstX; + + // Create an array suitable for holding one pixel + int[] ps = src.getPixel(0, 0, (int[])null); + int[] pd = dst.getPixel(0, 0, (int[])null); + + dstX = xOffset; + switch (process) { + case POST_NONE: + for (srcX = 0; srcX < width; srcX++) { + src.getPixel(srcX, 0, ps); + dst.setPixel(dstX, y, ps); + dstX += step; + } + break; + + case POST_GAMMA: + for (srcX = 0; srcX < width; srcX++) { + src.getPixel(srcX, 0, ps); + + for (int i = 0; i < inputBands; i++) { + int x = ps[i]; + ps[i] = gammaLut[x]; + } + + dst.setPixel(dstX, y, ps); + dstX += step; + } + break; + + case POST_GRAY_LUT: + for (srcX = 0; srcX < width; srcX++) { + src.getPixel(srcX, 0, ps); + + pd[0] = grayLut[ps[0]]; + + dst.setPixel(dstX, y, pd); + dstX += step; + } + break; + + case POST_GRAY_LUT_ADD_TRANS: + for (srcX = 0; srcX < width; srcX++) { + src.getPixel(srcX, 0, ps); + + int val = ps[0]; + pd[0] = grayLut[val]; + if (val == grayTransparentAlpha) { + pd[1] = 0; + } else { + pd[1] = maxOpacity; + } + + dst.setPixel(dstX, y, pd); + dstX += step; + } + break; + + case POST_PALETTE_TO_RGB: + for (srcX = 0; srcX < width; srcX++) { + src.getPixel(srcX, 0, ps); + + int val = ps[0]; + pd[0] = redPalette[val]; + pd[1] = greenPalette[val]; + pd[2] = bluePalette[val]; + + dst.setPixel(dstX, y, pd); + dstX += step; + } + break; + + case POST_PALETTE_TO_RGBA: + for (srcX = 0; srcX < width; srcX++) { + src.getPixel(srcX, 0, ps); + + int val = ps[0]; + pd[0] = redPalette[val]; + pd[1] = greenPalette[val]; + pd[2] = bluePalette[val]; + pd[3] = alphaPalette[val]; + + dst.setPixel(dstX, y, pd); + dstX += step; + } + break; + + case POST_ADD_GRAY_TRANS: + for (srcX = 0; srcX < width; srcX++) { + src.getPixel(srcX, 0, ps); + + int val = ps[0]; + if (performGammaCorrection) { + val = gammaLut[val]; + } + pd[0] = val; + if (val == grayTransparentAlpha) { + pd[1] = 0; + } else { + pd[1] = maxOpacity; + } + + dst.setPixel(dstX, y, pd); + dstX += step; + } + break; + + case POST_ADD_RGB_TRANS: + boolean flagGammaCorrection = performGammaCorrection; // local is cheaper + int[] workGammaLut = gammaLut; + for (srcX = 0; srcX < width; srcX++) { + src.getPixel(srcX, 0, ps); + + int r = ps[0]; + int g = ps[1]; + int b = ps[2]; + if (flagGammaCorrection) { + pd[0] = workGammaLut[r]; + pd[1] = workGammaLut[g]; + pd[2] = workGammaLut[b]; + } else { + pd[0] = r; + pd[1] = g; + pd[2] = b; + } + if ((r == redTransparentAlpha) + && (g == greenTransparentAlpha) + && (b == blueTransparentAlpha)) { + pd[3] = 0; + } else { + pd[3] = maxOpacity; + } + + dst.setPixel(dstX, y, pd); + dstX += step; + } + break; + + case POST_REMOVE_GRAY_TRANS: + for (srcX = 0; srcX < width; srcX++) { + src.getPixel(srcX, 0, ps); + + int g = ps[0]; + if (performGammaCorrection) { + pd[0] = gammaLut[g]; + } else { + pd[0] = g; + } + + dst.setPixel(dstX, y, pd); + dstX += step; + } + break; + + case POST_REMOVE_RGB_TRANS: + for (srcX = 0; srcX < width; srcX++) { + src.getPixel(srcX, 0, ps); + + int r = ps[0]; + int g = ps[1]; + int b = ps[2]; + if (performGammaCorrection) { + pd[0] = gammaLut[r]; + pd[1] = gammaLut[g]; + pd[2] = gammaLut[b]; + } else { + pd[0] = r; + pd[1] = g; + pd[2] = b; + } + + dst.setPixel(dstX, y, pd); + dstX += step; + } + break; + + case POST_GAMMA_EXP: + for (srcX = 0; srcX < width; srcX++) { + src.getPixel(srcX, 0, ps); + + int val = ps[0]; + int alpha = ps[1]; + int gamma = gammaLut[val]; + pd[0] = gamma; + pd[1] = gamma; + pd[2] = gamma; + pd[3] = alpha; + + dst.setPixel(dstX, y, pd); + dstX += step; + } + break; + + case POST_GRAY_ALPHA_EXP: + for (srcX = 0; srcX < width; srcX++) { + src.getPixel(srcX, 0, ps); + + int val = ps[0]; + int alpha = ps[1]; + pd[0] = val; + pd[1] = val; + pd[2] = val; + pd[3] = alpha; + + dst.setPixel(dstX, y, pd); + dstX += step; + } + break; + + case POST_ADD_GRAY_TRANS_EXP: + for (srcX = 0; srcX < width; srcX++) { + src.getPixel(srcX, 0, ps); + + int val = ps[0]; + if (performGammaCorrection) { + val = gammaLut[val]; + } + pd[0] = val; + pd[1] = val; + pd[2] = val; + if (val == grayTransparentAlpha) { + pd[3] = 0; + } else { + pd[3] = maxOpacity; + } + + dst.setPixel(dstX, y, pd); + dstX += step; + } + break; + + case POST_GRAY_LUT_ADD_TRANS_EXP: + for (srcX = 0; srcX < width; srcX++) { + src.getPixel(srcX, 0, ps); + + int val = ps[0]; + int val2 = grayLut[val]; + pd[0] = val2; + pd[1] = val2; + pd[2] = val2; + if (val == grayTransparentAlpha) { + pd[3] = 0; + } else { + pd[3] = maxOpacity; + } + + dst.setPixel(dstX, y, pd); + dstX += step; + } + break; + } + } + + /** + * Reads in an image of a given size and returns it as a + * WritableRaster. + */ + private void decodePass(WritableRaster imRas, + int xOffset, int yOffset, + int xStep, int yStep, + int passWidth, int passHeight) { + if ((passWidth == 0) || (passHeight == 0)) { + return; + } + + int bytesPerRow = (inputBands * passWidth * bitDepth + 7) / 8; + int eltsPerRow = (bitDepth == 16) ? bytesPerRow / 2 : bytesPerRow; + byte[] curr = new byte[bytesPerRow]; + byte[] prior = new byte[bytesPerRow]; + + // Create a 1-row tall Raster to hold the data + WritableRaster passRow = + createRaster(passWidth, 1, inputBands, + eltsPerRow, + bitDepth); + DataBuffer dataBuffer = passRow.getDataBuffer(); + int type = dataBuffer.getDataType(); + byte[] byteData = null; + short[] shortData = null; + if (type == DataBuffer.TYPE_BYTE) { + byteData = ((DataBufferByte)dataBuffer).getData(); + } else { + shortData = ((DataBufferUShort)dataBuffer).getData(); + } + + // Decode the (sub)image row-by-row + int srcY; + int dstY; + for (srcY = 0, dstY = yOffset; + srcY < passHeight; + srcY++, dstY += yStep) { + // Read the filter type byte and a row of data + int filter = 0; + try { + filter = dataStream.read(); + dataStream.readFully(curr, 0, bytesPerRow); + } catch (Exception e) { + e.printStackTrace(); + } + + switch (filter) { + case PNG_FILTER_NONE: + break; + case PNG_FILTER_SUB: + decodeSubFilter(curr, bytesPerRow, bytesPerPixel); + break; + case PNG_FILTER_UP: + decodeUpFilter(curr, prior, bytesPerRow); + break; + case PNG_FILTER_AVERAGE: + decodeAverageFilter(curr, prior, bytesPerRow, bytesPerPixel); + break; + case PNG_FILTER_PAETH: + decodePaethFilter(curr, prior, bytesPerRow, bytesPerPixel); + break; + default: + // Error -- unknown filter type + String msg = PropertyUtil.getString("PNGImageDecoder16"); + throw new RuntimeException(msg); + } + + // Copy data into passRow byte by byte + if (bitDepth < 16) { + System.arraycopy(curr, 0, byteData, 0, bytesPerRow); + } else { + int idx = 0; + for (int j = 0; j < eltsPerRow; j++) { + shortData[j] = + (short)((curr[idx] << 8) | (curr[idx + 1] & 0xff)); + idx += 2; + } + } + + processPixels(postProcess, + passRow, imRas, xOffset, xStep, dstY, passWidth); + + // Swap curr and prior + byte[] tmp = prior; + prior = curr; + curr = tmp; + } + } + + private void decodeImage(boolean useInterlacing) { + int width = bounds.width; + int height = bounds.height; + + if (!useInterlacing) { + decodePass(theTile, 0, 0, 1, 1, width, height); + } else { + decodePass(theTile, 0, 0, 8, 8, (width + 7) / 8, (height + 7) / 8); + decodePass(theTile, 4, 0, 8, 8, (width + 3) / 8, (height + 7) / 8); + decodePass(theTile, 0, 4, 4, 8, (width + 3) / 4, (height + 3) / 8); + decodePass(theTile, 2, 0, 4, 4, (width + 1) / 4, (height + 3) / 4); + decodePass(theTile, 0, 2, 2, 4, (width + 1) / 2, (height + 1) / 4); + decodePass(theTile, 1, 0, 2, 2, width / 2, (height + 1) / 2); + decodePass(theTile, 0, 1, 1, 2, width, height / 2); + } + } + + public WritableRaster copyData(WritableRaster wr) { + GraphicsUtil.copyData(theTile, wr); + return wr; + } + + // RenderedImage stuff + @Override + public Raster getTile(int tileX, int tileY) { + if (tileX != 0 || tileY != 0) { + // Error -- bad tile requested + String msg = PropertyUtil.getString("PNGImageDecoder17"); + throw new IllegalArgumentException(msg); + } + return theTile; + } +} diff --git a/src/main/java/org/apache/xmlgraphics/image/codec/png/PNGSuggestedPaletteEntry.java b/src/main/java/org/apache/xmlgraphics/image/codec/png/PNGSuggestedPaletteEntry.java new file mode 100644 index 0000000..e38d54e --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/codec/png/PNGSuggestedPaletteEntry.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PNGSuggestedPaletteEntry.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.image.codec.png; + +import java.io.Serializable; + +/** + * A class representing the fields of a PNG suggested palette entry. + * + *

    This class is not a committed part of the JAI API. It may + * be removed or changed in future releases of JAI. + */ +public class PNGSuggestedPaletteEntry implements Serializable { + + private static final long serialVersionUID = 8718480055883536195L; + /** The name of the entry. */ + public String name; + + /** The depth of the color samples. */ + public int sampleDepth; + + /** The red color value of the entry. */ + public int red; + + /** The green color value of the entry. */ + public int green; + + /** The blue color value of the entry. */ + public int blue; + + /** The alpha opacity value of the entry. */ + public int alpha; + + /** The probable frequency of the color in the image. */ + public int frequency; +} diff --git a/src/main/java/org/apache/xmlgraphics/image/codec/png/package.html b/src/main/java/org/apache/xmlgraphics/image/codec/png/package.html new file mode 100644 index 0000000..3e47393 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/codec/png/package.html @@ -0,0 +1,23 @@ + + + +org.apache.xmlgraphics.image.codec.png Package + +

    Contains a PNG image codec.

    + + \ No newline at end of file diff --git a/src/main/java/org/apache/xmlgraphics/image/codec/tiff/CompressionValue.java b/src/main/java/org/apache/xmlgraphics/image/codec/tiff/CompressionValue.java new file mode 100644 index 0000000..b1fe2ed --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/codec/tiff/CompressionValue.java @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.xmlgraphics.image.codec.tiff; + + +/** Enumerates the possible compression values for TIFF images. */ +public enum CompressionValue { + /** No compression. */ + NONE(1), + /** + * Modified Huffman Compression (CCITT Group 3 1D facsimile compression). + *

    Not currently supported. + */ + GROUP3_1D(2), + /** + * CCITT T.4 bilevel compression (CCITT Group 3 2D facsimile compression). + *

    Not currently supported. + */ + GROUP3_2D(3), + /** + * CCITT T.6 bilevel compression (CCITT Group 4 facsimile compression). + *

    Not currently supported. + */ + GROUP4(4), + /** LZW compression.

    Not supported. */ + LZW(5), + /** + * Code for original JPEG-in-TIFF compression which has been depricated (for many good reasons) + * in favor of Tech Note 2 JPEG compression (compression scheme 7). + *

    Not supported. + */ + JPEG_BROKEN(6), + /** JPEG-in-TIFF compression. */ + JPEG_TTN2(7), + /** Byte-oriented run-length encoding "PackBits" compression. */ + PACKBITS(32773), + /** + * + * DEFLATE lossless compression (also known as "Zip-in-TIFF"). + */ + DEFLATE(32946); + + private final int compressionValue; + + private CompressionValue(int compressionValue) { + this.compressionValue = compressionValue; + } + + int getValue() { + return compressionValue; + } + + /** + * Gets the compression value given the name of the compression type. + * @param name the compression name + * @return the compression value + */ + public static CompressionValue getValue(String name) { + if (name == null) { + return PACKBITS; + } + for (CompressionValue cv : CompressionValue.values()) { + if (cv.toString().equalsIgnoreCase(name)) { + return cv; + } + } + throw new IllegalArgumentException("Unknown compression value: " + name); + } +} diff --git a/src/main/java/org/apache/xmlgraphics/image/codec/tiff/ExtraSamplesType.java b/src/main/java/org/apache/xmlgraphics/image/codec/tiff/ExtraSamplesType.java new file mode 100644 index 0000000..0afec77 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/codec/tiff/ExtraSamplesType.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.xmlgraphics.image.codec.tiff; + +import java.awt.image.ColorModel; + +enum ExtraSamplesType { + UNSPECIFIED(0), + ASSOCIATED_ALPHA(1), + UNASSOCIATED_ALPHA(2); + + private final int typeValue; + + private ExtraSamplesType(int value) { + this.typeValue = value; + } + + static ExtraSamplesType getValue(ColorModel colorModel, int numExtraSamples) { + if (numExtraSamples == 1 && colorModel.hasAlpha()) { + return colorModel.isAlphaPremultiplied() ? ASSOCIATED_ALPHA : UNASSOCIATED_ALPHA; + } + return UNSPECIFIED; + } + + int getValue() { + return typeValue; + } +} diff --git a/src/main/java/org/apache/xmlgraphics/image/codec/tiff/ImageInfo.java b/src/main/java/org/apache/xmlgraphics/image/codec/tiff/ImageInfo.java new file mode 100644 index 0000000..f30286a --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/codec/tiff/ImageInfo.java @@ -0,0 +1,213 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.xmlgraphics.image.codec.tiff; + +import java.awt.color.ColorSpace; +import java.awt.image.ColorModel; +import java.awt.image.IndexColorModel; +import java.awt.image.RenderedImage; + +final class ImageInfo { + + // Default values + private static final int DEFAULT_ROWS_PER_STRIP = 8; + + private final int numExtraSamples; + private final ExtraSamplesType extraSampleType; + private final ImageType imageType; + private final int colormapSize; + private final char[] colormap; + private final int tileWidth; + private final int tileHeight; + private final int numTiles; + private final long bytesPerRow; + private final long bytesPerTile; + + private ImageInfo(ImageInfoBuilder builder) { + this.numExtraSamples = builder.numExtraSamples; + this.extraSampleType = builder.extraSampleType; + this.imageType = builder.imageType; + this.colormapSize = builder.colormapSize; + this.colormap = copyColormap(builder.colormap); + this.tileWidth = builder.tileWidth; + this.tileHeight = builder.tileHeight; + this.numTiles = builder.numTiles; + this.bytesPerRow = builder.bytesPerRow; + this.bytesPerTile = builder.bytesPerTile; + } + + private static char[] copyColormap(char[] colorMap) { + if (colorMap == null) { + return null; + } + char[] copy = new char[colorMap.length]; + System.arraycopy(colorMap, 0, copy, 0, colorMap.length); + return copy; + } + + private static int getNumberOfExtraSamplesForColorSpace(ColorSpace colorSpace, + ImageType imageType, int numBands) { + if (imageType == ImageType.GENERIC) { + return numBands - 1; + } else if (numBands > 1) { + return numBands - colorSpace.getNumComponents(); + } else { + return 0; + } + } + + private static char[] createColormap(final int sizeOfColormap, byte[] r, byte[] g, byte[] b) { + int redIndex = 0; + int greenIndex = sizeOfColormap; + int blueIndex = 2 * sizeOfColormap; + char[] colormap = new char[sizeOfColormap * 3]; + for (int i = 0; i < sizeOfColormap; i++) { + // beware of sign extended bytes + colormap[redIndex++] = convertColorToColormapChar(0xff & r[i]); + colormap[greenIndex++] = convertColorToColormapChar(0xff & g[i]); + colormap[blueIndex++] = convertColorToColormapChar(0xff & b[i]); + } + return colormap; + } + + private static char convertColorToColormapChar(int color) { + return (char) (color << 8 | color); + } + + int getNumberOfExtraSamples() { + return numExtraSamples; + } + + ExtraSamplesType getExtraSamplesType() { + return extraSampleType; + } + + ImageType getType() { + return imageType; + } + + int getColormapSize() { + return colormapSize; + } + + char[] getColormap() { + return copyColormap(colormap); + } + + int getTileWidth() { + return tileWidth; + } + + int getTileHeight() { + return tileHeight; + } + + int getNumTiles() { + return numTiles; + } + + long getBytesPerRow() { + return bytesPerRow; + } + + long getBytesPerTile() { + return bytesPerTile; + } + + static ImageInfo newInstance(RenderedImage im, int dataTypeSize, int numBands, + ColorModel colorModel, TIFFEncodeParam params) { + ImageInfoBuilder builder = new ImageInfoBuilder(); + if (colorModel instanceof IndexColorModel) { // Bilevel or palette + IndexColorModel indexColorModel = (IndexColorModel) colorModel; + int colormapSize = indexColorModel.getMapSize(); + byte[] r = new byte[colormapSize]; + indexColorModel.getReds(r); + byte[] g = new byte[colormapSize]; + indexColorModel.getGreens(g); + byte[] b = new byte[colormapSize]; + indexColorModel.getBlues(b); + + builder.imageType = ImageType.getTypeFromRGB(colormapSize, r, g, b, dataTypeSize, + numBands); + if (builder.imageType == ImageType.PALETTE) { + builder.colormap = createColormap(colormapSize, r, g, b); + builder.colormapSize = colormapSize * 3; + } + } else if (colorModel == null) { + if (dataTypeSize == 1 && numBands == 1) { // bilevel + builder.imageType = ImageType.BILEVEL_BLACK_IS_ZERO; + } else { + builder.imageType = ImageType.GENERIC; + builder.numExtraSamples = numBands > 1 ? numBands - 1 : 0; + } + } else { + ColorSpace colorSpace = colorModel.getColorSpace(); + builder.imageType = ImageType.getTypeFromColorSpace(colorSpace, params); + builder.numExtraSamples = getNumberOfExtraSamplesForColorSpace(colorSpace, + builder.imageType, numBands); + builder.extraSampleType = ExtraSamplesType.getValue(colorModel, + builder.numExtraSamples); + } + + // Initialize tile dimensions. + final int width = im.getWidth(); + final int height = im.getHeight(); + if (params.getWriteTiled()) { + builder.tileWidth = params.getTileWidth() > 0 ? params.getTileWidth() : width; + builder.tileHeight = params.getTileHeight() > 0 ? params.getTileHeight() : height; + // NB: Parentheses are used in this statement for correct rounding. + builder.numTiles = ((width + builder.tileWidth - 1) / builder.tileWidth) + * ((height + builder.tileHeight - 1) / builder.tileHeight); + } else { + builder.tileWidth = width; + builder.tileHeight = params.getTileHeight() > 0 ? params.getTileHeight() + : DEFAULT_ROWS_PER_STRIP; + builder.numTiles = (int) Math.ceil(height / (double) builder.tileHeight); + } + builder.setBytesPerRow(dataTypeSize, numBands) + .setBytesPerTile(); + return builder.build(); + } + + private static final class ImageInfoBuilder { + private ImageType imageType = ImageType.UNSUPPORTED; + private int numExtraSamples; + private char[] colormap; + private int colormapSize; + private ExtraSamplesType extraSampleType = ExtraSamplesType.UNSPECIFIED; + private int tileWidth; + private int tileHeight; + private int numTiles; + private long bytesPerRow; + private long bytesPerTile; + + private ImageInfoBuilder setBytesPerRow(int dataTypeSize, int numBands) { + bytesPerRow = (long) Math.ceil((dataTypeSize / 8.0) * tileWidth * numBands); + return this; + } + + private ImageInfoBuilder setBytesPerTile() { + bytesPerTile = bytesPerRow * tileHeight; + return this; + } + + private ImageInfo build() { + return new ImageInfo(this); + } + } +} diff --git a/src/main/java/org/apache/xmlgraphics/image/codec/tiff/ImageType.java b/src/main/java/org/apache/xmlgraphics/image/codec/tiff/ImageType.java new file mode 100644 index 0000000..188ea1f --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/codec/tiff/ImageType.java @@ -0,0 +1,103 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.xmlgraphics.image.codec.tiff; + +import java.awt.color.ColorSpace; + +import org.apache.xmlgraphics.image.codec.util.PropertyUtil; + +enum ImageType { + UNSUPPORTED(-1), + BILEVEL_WHITE_IS_ZERO(0), + BILEVEL_BLACK_IS_ZERO(1), + GRAY(1), + PALETTE(3), + RGB(2), + CMYK(5), + YCBCR(6), + CIELAB(8), + GENERIC(1); + + private final int photometricInterpretation; + + private ImageType(int photometricInterpretation) { + this.photometricInterpretation = photometricInterpretation; + } + + int getPhotometricInterpretation() { + return photometricInterpretation; + } + + static ImageType getTypeFromRGB(int mapSize, byte[] r, byte[] g, byte[] b, + int dataTypeSize, int numBands) { + if (numBands == 1) { + if (dataTypeSize == 1) { // Bilevel image + if (mapSize != 2) { + throw new IllegalArgumentException(PropertyUtil.getString("TIFFImageEncoder7")); + } + + if (isBlackZero(r, g, b)) { + return BILEVEL_BLACK_IS_ZERO; + } else if (isWhiteZero(r, g, b)) { + return BILEVEL_WHITE_IS_ZERO; + } + } + return PALETTE; + } + return UNSUPPORTED; + } + + private static boolean rgbIsValueAt(byte[] r, byte[] g, byte[] b, byte value, int i) { + return r[i] == value && g[i] == value && b[i] == value; + } + + private static boolean bilevelColorValue(byte[] r, byte[] g, byte[] b, int blackValue, + int whiteValue) { + return rgbIsValueAt(r, g, b, (byte) blackValue, 0) + && rgbIsValueAt(r, g, b, (byte) whiteValue, 1); + } + + private static boolean isBlackZero(byte[] r, byte[] g, byte[] b) { + return bilevelColorValue(r, g, b, 0, 0xff); + } + + private static boolean isWhiteZero(byte[] r, byte[] g, byte[] b) { + return bilevelColorValue(r, g, b, 0xff, 0); + } + + static ImageType getTypeFromColorSpace(ColorSpace colorSpace, TIFFEncodeParam params) { + switch (colorSpace.getType()) { + case ColorSpace.TYPE_CMYK: + return CMYK; + case ColorSpace.TYPE_GRAY: + return GRAY; + case ColorSpace.TYPE_Lab: + return CIELAB; + case ColorSpace.TYPE_RGB: + if (params.getJPEGCompressRGBToYCbCr()) { + return ImageType.YCBCR; + } else { + return ImageType.RGB; + } + case ColorSpace.TYPE_YCbCr: + return YCBCR; + default: + return GENERIC; + } + } +} diff --git a/src/main/java/org/apache/xmlgraphics/image/codec/tiff/TIFFDecodeParam.java b/src/main/java/org/apache/xmlgraphics/image/codec/tiff/TIFFDecodeParam.java new file mode 100644 index 0000000..f85b844 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/codec/tiff/TIFFDecodeParam.java @@ -0,0 +1,160 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: TIFFDecodeParam.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.image.codec.tiff; + +import org.apache.xmlgraphics.image.codec.util.ImageDecodeParam; + +/** + * An instance of ImageDecodeParam for decoding images in + * the TIFF format. + * + *

    To determine the number of images present in a TIFF file, use + * the getNumPages() method on the + * ImageDecoder object that will be used to perform the + * decoding. The desired page number may be passed as an argument to + * the ImageDecoder.decodeAsRaster)() or + * decodeAsRenderedImage() methods. + * + *

    For TIFF Palette color images, the colorMap always has entries + * of short data type, the color Black being represented by 0,0,0 and + * White by 65536,65536,65536. In order to display these images, the + * default behavior is to dither the short values down to 8 bits. + * The dithering is done by calling the decode16BitsTo8Bits + * method for each short value that needs to be dithered. The method has + * the following implementation: + * + * byte b; + * short s; + * s = s & 0xffff; + * b = (byte)((s >> 8) & 0xff); + * + * If a different algorithm is to be used for the dithering, this class + * should be subclassed and an appropriate implementation should be + * provided for the decode16BitsTo8Bits method in the subclass. + * + *

    If the palette contains image data that is signed short, as specified + * by the SampleFormat tag, the dithering is done by calling + * decodeSigned16BitsTo8Bits instead. The method has the + * following implementation: + * + * byte b; + * short s; + * b = (byte)((s + Short.MIN_VALUE) >> 8); + * + * In order to use a different algorithm for the dithering, this class + * should be subclassed and the method overridden. + * + *

    If it is desired that the Palette be decoded such that the output + * image is of short data type and no dithering is performed, the + * setDecodePaletteAsShorts method should be used. + * + *

    This class is not a committed part of the JAI API. It may + * be removed or changed in future releases of JAI. + * + * @see TIFFDirectory + */ +public class TIFFDecodeParam implements ImageDecodeParam { + + private static final long serialVersionUID = -2371665950056848358L; + private boolean decodePaletteAsShorts; + private Long ifdOffset; + private boolean convertJPEGYCbCrToRGB = true; + + /** Constructs a default instance of TIFFDecodeParam. */ + public TIFFDecodeParam() { + } + + /** + * If set, the entries in the palette will be decoded as shorts + * and no short to byte lookup will be applied to them. + */ + public void setDecodePaletteAsShorts(boolean decodePaletteAsShorts) { + this.decodePaletteAsShorts = decodePaletteAsShorts; + } + + /** + * Returns true if palette entries will be decoded as + * shorts, resulting in an output image with short datatype. + */ + public boolean getDecodePaletteAsShorts() { + return decodePaletteAsShorts; + } + + /** + * Returns an unsigned 8 bit value computed by dithering the unsigned + * 16 bit value. Note that the TIFF specified short datatype is an + * unsigned value, while Java's short datatype is a + * signed value. Therefore the Java short datatype cannot + * be used to store the TIFF specified short value. A Java + * int is used as input instead to this method. The method + * deals correctly only with 16 bit unsigned values. + */ + public byte decode16BitsTo8Bits(int s) { + return (byte)((s >> 8) & 0xffff); + } + + /** + * Returns an unsigned 8 bit value computed by dithering the signed + * 16 bit value. This method deals correctly only with values in the + * 16 bit signed range. + */ + public byte decodeSigned16BitsTo8Bits(short s) { + return (byte)((s + Short.MIN_VALUE) >> 8); + } + + /** + * Sets the offset in the stream from which to read the image. There + * must be an Image File Directory (IFD) at that position or an error + * will occur. If setIFDOffset() is never invoked then + * the decoder will assume that the TIFF stream is at the beginning of + * the 8-byte image header. If the directory offset is set and a page + * number is supplied to the TIFF ImageDecoder then the + * page will be the zero-relative index of the IFD in linked list of + * IFDs beginning at the specified offset with a page of zero indicating + * the directory at that offset. + */ + public void setIFDOffset(long offset) { + ifdOffset = offset; + } + + /** + * Returns the value set by setIFDOffset() or + * null if no value has been set. + */ + public Long getIFDOffset() { + return ifdOffset; + } + + /** + * Sets a flag indicating whether to convert JPEG-compressed YCbCr data + * to RGB. The default value is true. This flag is + * ignored if the image data are not JPEG-compressed. + */ + public void setJPEGDecompressYCbCrToRGB(boolean convertJPEGYCbCrToRGB) { + this.convertJPEGYCbCrToRGB = convertJPEGYCbCrToRGB; + } + + /** + * Whether JPEG-compressed YCbCr data will be converted to RGB. + */ + public boolean getJPEGDecompressYCbCrToRGB() { + return convertJPEGYCbCrToRGB; + } +} diff --git a/src/main/java/org/apache/xmlgraphics/image/codec/tiff/TIFFDirectory.java b/src/main/java/org/apache/xmlgraphics/image/codec/tiff/TIFFDirectory.java new file mode 100644 index 0000000..1cf0ae9 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/codec/tiff/TIFFDirectory.java @@ -0,0 +1,641 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: TIFFDirectory.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.image.codec.tiff; + +import java.io.IOException; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.apache.xmlgraphics.image.codec.util.PropertyUtil; +import org.apache.xmlgraphics.image.codec.util.SeekableStream; + +// CSOFF: ConstantName +// CSOFF: EmptyStatement +// CSOFF: InnerAssignment +// CSOFF: LocalVariableName +// CSOFF: MemberName +// CSOFF: MultipleVariableDeclarations +// CSOFF: NeedBraces +// CSOFF: ParameterName +// CSOFF: WhitespaceAround + +/** + * A class representing an Image File Directory (IFD) from a TIFF 6.0 + * stream. The TIFF file format is described in more detail in the + * comments for the TIFFDescriptor class. + * + *

    A TIFF IFD consists of a set of TIFFField tags. Methods are + * provided to query the set of tags and to obtain the raw field + * array. In addition, convenience methods are provided for acquiring + * the values of tags that contain a single value that fits into a + * byte, int, long, float, or double. + * + *

    Every TIFF file is made up of one or more public IFDs that are + * joined in a linked list, rooted in the file header. A file may + * also contain so-called private IFDs that are referenced from + * tag data and do not appear in the main list. + * + *

    This class is not a committed part of the JAI API. It may + * be removed or changed in future releases of JAI. + * + * @see TIFFField + * @version $Id: TIFFDirectory.java 1732018 2016-02-24 04:51:06Z gadams $ + */ +public class TIFFDirectory implements Serializable { + + private static final long serialVersionUID = 2007844835460959003L; + /** A boolean storing the endianness of the stream. */ + boolean isBigEndian; + + /** The number of entries in the IFD. */ + int numEntries; + + /** An array of TIFFFields. */ + TIFFField[] fields; + + /** A Hashtable indexing the fields by tag number. */ + Map fieldIndex = new HashMap(); + + /** The offset of this IFD. */ + long ifdOffset = 8; + + /** The offset of the next IFD. */ + long nextIFDOffset; + + /** The default constructor. */ + TIFFDirectory() { } + + private static boolean isValidEndianTag(int endian) { + return ((endian == 0x4949) || (endian == 0x4d4d)); + } + + /** + * Constructs a TIFFDirectory from a SeekableStream. + * The directory parameter specifies which directory to read from + * the linked list present in the stream; directory 0 is normally + * read but it is possible to store multiple images in a single + * TIFF file by maintaing multiple directories. + * + * @param stream a SeekableStream to read from. + * @param directory the index of the directory to read. + */ + public TIFFDirectory(SeekableStream stream, int directory) + throws IOException { + + long globalSaveOffset = stream.getFilePointer(); + long ifdOffset; + + // Read the TIFF header + stream.seek(0L); + int endian = stream.readUnsignedShort(); + if (!isValidEndianTag(endian)) { + throw new IllegalArgumentException(PropertyUtil.getString("TIFFDirectory1")); + } + isBigEndian = (endian == 0x4d4d); + + int magic = readUnsignedShort(stream); + if (magic != 42) { + throw new IllegalArgumentException(PropertyUtil.getString("TIFFDirectory2")); + } + + // Get the initial ifd offset as an unsigned int (using a long) + ifdOffset = readUnsignedInt(stream); + + for (int i = 0; i < directory; i++) { + if (ifdOffset == 0L) { + throw new IllegalArgumentException(PropertyUtil.getString("TIFFDirectory3")); + } + + stream.seek(ifdOffset); + long entries = readUnsignedShort(stream); + stream.skip(12 * entries); + + ifdOffset = readUnsignedInt(stream); + } + if (ifdOffset == 0L) { + throw new IllegalArgumentException(PropertyUtil.getString("TIFFDirectory3")); + } + + stream.seek(ifdOffset); + initialize(stream); + stream.seek(globalSaveOffset); + } + + /** + * Constructs a TIFFDirectory by reading a SeekableStream. + * The ifd_offset parameter specifies the stream offset from which + * to begin reading; this mechanism is sometimes used to store + * private IFDs within a TIFF file that are not part of the normal + * sequence of IFDs. + * + * @param stream a SeekableStream to read from. + * @param ifdOffset the long byte offset of the directory. + * @param directory the index of the directory to read beyond the + * one at the current stream offset; zero indicates the IFD + * at the current offset. + */ + public TIFFDirectory(SeekableStream stream, long ifdOffset, int directory) + throws IOException { + + long globalSaveOffset = stream.getFilePointer(); + stream.seek(0L); + int endian = stream.readUnsignedShort(); + if (!isValidEndianTag(endian)) { + throw new IllegalArgumentException(PropertyUtil.getString("TIFFDirectory1")); + } + isBigEndian = (endian == 0x4d4d); + + // Seek to the first IFD. + stream.seek(ifdOffset); + + // Seek to desired IFD if necessary. + int dirNum = 0; + while (dirNum < directory) { + // Get the number of fields in the current IFD. + long numEntries = readUnsignedShort(stream); + + // Skip to the next IFD offset value field. + stream.seek(ifdOffset + 12 * numEntries); + + // Read the offset to the next IFD beyond this one. + ifdOffset = readUnsignedInt(stream); + + // Seek to the next IFD. + stream.seek(ifdOffset); + + // Increment the directory. + dirNum++; + } + + initialize(stream); + stream.seek(globalSaveOffset); + } + + private static final int[] SIZE_OF_TYPE = { + 0, // 0 = n/a + 1, // 1 = byte + 1, // 2 = ascii + 2, // 3 = short + 4, // 4 = long + 8, // 5 = rational + 1, // 6 = sbyte + 1, // 7 = undefined + 2, // 8 = sshort + 4, // 9 = slong + 8, // 10 = srational + 4, // 11 = float + 8 // 12 = double + }; + + private void initialize(SeekableStream stream) throws IOException { + long nextTagOffset; + int i; + int j; + + ifdOffset = stream.getFilePointer(); + + numEntries = readUnsignedShort(stream); + fields = new TIFFField[numEntries]; + + for (i = 0; i < numEntries; i++) { + int tag = readUnsignedShort(stream); + int type = readUnsignedShort(stream); + int count = (int)(readUnsignedInt(stream)); + int value = 0; + + // The place to return to to read the next tag + nextTagOffset = stream.getFilePointer() + 4; + + try { + // If the tag data can't fit in 4 bytes, the next 4 bytes + // contain the starting offset of the data + if (count * SIZE_OF_TYPE[type] > 4) { + value = (int)(readUnsignedInt(stream)); + stream.seek(value); + } + } catch (ArrayIndexOutOfBoundsException ae) { + // System.err.println(tag + " " + "TIFFDirectory4"); TODO - log this message + // if the data type is unknown we should skip this TIFF Field + stream.seek(nextTagOffset); + continue; + } + + fieldIndex.put(tag, i); + Object obj = null; + + switch (type) { + case TIFFField.TIFF_BYTE: + case TIFFField.TIFF_SBYTE: + case TIFFField.TIFF_UNDEFINED: + case TIFFField.TIFF_ASCII: + byte[] bvalues = new byte[count]; + stream.readFully(bvalues, 0, count); + + if (type == TIFFField.TIFF_ASCII) { + + // Can be multiple strings + int index = 0; + int prevIndex = 0; + List v = new ArrayList(); + + while (index < count) { + + while ((index < count) && (bvalues[index++] != 0)) { + // NOP + } + + // When we encountered zero, means one string has ended + v.add(new String(bvalues, prevIndex, + (index - prevIndex), "UTF-8")); + prevIndex = index; + } + + count = v.size(); + String[] strings = new String[count]; + v.toArray(strings); + obj = strings; + } else { + obj = bvalues; + } + + break; + + case TIFFField.TIFF_SHORT: + char[] cvalues = new char[count]; + for (j = 0; j < count; j++) { + cvalues[j] = (char)(readUnsignedShort(stream)); + } + obj = cvalues; + break; + + case TIFFField.TIFF_LONG: + long[] lvalues = new long[count]; + for (j = 0; j < count; j++) { + lvalues[j] = readUnsignedInt(stream); + } + obj = lvalues; + break; + + case TIFFField.TIFF_RATIONAL: + long[][] llvalues = new long[count][2]; + for (j = 0; j < count; j++) { + llvalues[j][0] = readUnsignedInt(stream); + llvalues[j][1] = readUnsignedInt(stream); + } + obj = llvalues; + break; + + case TIFFField.TIFF_SSHORT: + short[] svalues = new short[count]; + for (j = 0; j < count; j++) { + svalues[j] = readShort(stream); + } + obj = svalues; + break; + + case TIFFField.TIFF_SLONG: + int[] ivalues = new int[count]; + for (j = 0; j < count; j++) { + ivalues[j] = readInt(stream); + } + obj = ivalues; + break; + + case TIFFField.TIFF_SRATIONAL: + int[][] iivalues = new int[count][2]; + for (j = 0; j < count; j++) { + iivalues[j][0] = readInt(stream); + iivalues[j][1] = readInt(stream); + } + obj = iivalues; + break; + + case TIFFField.TIFF_FLOAT: + float[] fvalues = new float[count]; + for (j = 0; j < count; j++) { + fvalues[j] = readFloat(stream); + } + obj = fvalues; + break; + + case TIFFField.TIFF_DOUBLE: + double[] dvalues = new double[count]; + for (j = 0; j < count; j++) { + dvalues[j] = readDouble(stream); + } + obj = dvalues; + break; + + default: + throw new RuntimeException(PropertyUtil.getString("TIFFDirectory0")); + } + + fields[i] = new TIFFField(tag, type, count, obj); + stream.seek(nextTagOffset); + } + + // Read the offset of the next IFD. + nextIFDOffset = readUnsignedInt(stream); + } + + /** Returns the number of directory entries. */ + public int getNumEntries() { + return numEntries; + } + + /** + * Returns the value of a given tag as a TIFFField, + * or null if the tag is not present. + */ + public TIFFField getField(int tag) { + Integer i = (Integer)fieldIndex.get(tag); + if (i == null) { + return null; + } else { + return fields[i]; + } + } + + /** + * Returns true if a tag appears in the directory. + */ + public boolean isTagPresent(int tag) { + return fieldIndex.containsKey(tag); + } + + /** + * Returns an ordered array of ints indicating the tag + * values. + */ + public int[] getTags() { + int[] tags = new int[fieldIndex.size()]; + Iterator iter = fieldIndex.keySet().iterator(); + int i = 0; + + while (iter.hasNext()) { + tags[i++] = (Integer) iter.next(); + } + + return tags; + } + + /** + * Returns an array of TIFFFields containing all the fields + * in this directory. + */ + public TIFFField[] getFields() { + return fields; + } + + /** + * Returns the value of a particular index of a given tag as a + * byte. The caller is responsible for ensuring that the tag is + * present and has type TIFFField.TIFF_SBYTE, TIFF_BYTE, or + * TIFF_UNDEFINED. + */ + public byte getFieldAsByte(int tag, int index) { + Integer i = (Integer)fieldIndex.get(tag); + byte [] b = (fields[i]).getAsBytes(); + return b[index]; + } + + /** + * Returns the value of index 0 of a given tag as a + * byte. The caller is responsible for ensuring that the tag is + * present and has type TIFFField.TIFF_SBYTE, TIFF_BYTE, or + * TIFF_UNDEFINED. + */ + public byte getFieldAsByte(int tag) { + return getFieldAsByte(tag, 0); + } + + /** + * Returns the value of a particular index of a given tag as a + * long. The caller is responsible for ensuring that the tag is + * present and has type TIFF_BYTE, TIFF_SBYTE, TIFF_UNDEFINED, + * TIFF_SHORT, TIFF_SSHORT, TIFF_SLONG or TIFF_LONG. + */ + public long getFieldAsLong(int tag, int index) { + Integer i = (Integer)fieldIndex.get(tag); + return (fields[i]).getAsLong(index); + } + + /** + * Returns the value of index 0 of a given tag as a + * long. The caller is responsible for ensuring that the tag is + * present and has type TIFF_BYTE, TIFF_SBYTE, TIFF_UNDEFINED, + * TIFF_SHORT, TIFF_SSHORT, TIFF_SLONG or TIFF_LONG. + */ + public long getFieldAsLong(int tag) { + return getFieldAsLong(tag, 0); + } + + /** + * Returns the value of a particular index of a given tag as a + * float. The caller is responsible for ensuring that the tag is + * present and has numeric type (all but TIFF_UNDEFINED and + * TIFF_ASCII). + */ + public float getFieldAsFloat(int tag, int index) { + Integer i = (Integer)fieldIndex.get(tag); + return fields[i].getAsFloat(index); + } + + /** + * Returns the value of index 0 of a given tag as a float. The + * caller is responsible for ensuring that the tag is present and + * has numeric type (all but TIFF_UNDEFINED and TIFF_ASCII). + */ + public float getFieldAsFloat(int tag) { + return getFieldAsFloat(tag, 0); + } + + /** + * Returns the value of a particular index of a given tag as a + * double. The caller is responsible for ensuring that the tag is + * present and has numeric type (all but TIFF_UNDEFINED and + * TIFF_ASCII). + */ + public double getFieldAsDouble(int tag, int index) { + Integer i = (Integer)fieldIndex.get(tag); + return fields[i].getAsDouble(index); + } + + /** + * Returns the value of index 0 of a given tag as a double. The + * caller is responsible for ensuring that the tag is present and + * has numeric type (all but TIFF_UNDEFINED and TIFF_ASCII). + */ + public double getFieldAsDouble(int tag) { + return getFieldAsDouble(tag, 0); + } + + // Methods to read primitive data types from the stream + + private short readShort(SeekableStream stream) + throws IOException { + if (isBigEndian) { + return stream.readShort(); + } else { + return stream.readShortLE(); + } + } + + private int readUnsignedShort(SeekableStream stream) + throws IOException { + if (isBigEndian) { + return stream.readUnsignedShort(); + } else { + return stream.readUnsignedShortLE(); + } + } + + private int readInt(SeekableStream stream) + throws IOException { + if (isBigEndian) { + return stream.readInt(); + } else { + return stream.readIntLE(); + } + } + + private long readUnsignedInt(SeekableStream stream) + throws IOException { + if (isBigEndian) { + return stream.readUnsignedInt(); + } else { + return stream.readUnsignedIntLE(); + } + } + +// private long readLong(SeekableStream stream) +// throws IOException { +// if (isBigEndian) { +// return stream.readLong(); +// } else { +// return stream.readLongLE(); +// } +// } + + private float readFloat(SeekableStream stream) + throws IOException { + if (isBigEndian) { + return stream.readFloat(); + } else { + return stream.readFloatLE(); + } + } + + private double readDouble(SeekableStream stream) + throws IOException { + if (isBigEndian) { + return stream.readDouble(); + } else { + return stream.readDoubleLE(); + } + } + + private static int readUnsignedShort(SeekableStream stream, + boolean isBigEndian) + throws IOException { + if (isBigEndian) { + return stream.readUnsignedShort(); + } else { + return stream.readUnsignedShortLE(); + } + } + + private static long readUnsignedInt(SeekableStream stream, + boolean isBigEndian) + throws IOException { + if (isBigEndian) { + return stream.readUnsignedInt(); + } else { + return stream.readUnsignedIntLE(); + } + } + + // Utilities + + /** + * Returns the number of image directories (subimages) stored in a + * given TIFF file, represented by a SeekableStream. + */ + public static int getNumDirectories(SeekableStream stream) + throws IOException { + long pointer = stream.getFilePointer(); // Save stream pointer + + stream.seek(0L); + int endian = stream.readUnsignedShort(); + if (!isValidEndianTag(endian)) { + throw new IllegalArgumentException(PropertyUtil.getString("TIFFDirectory1")); + } + boolean isBigEndian = (endian == 0x4d4d); + int magic = readUnsignedShort(stream, isBigEndian); + if (magic != 42) { + throw new IllegalArgumentException(PropertyUtil.getString("TIFFDirectory2")); + } + + stream.seek(4L); + long offset = readUnsignedInt(stream, isBigEndian); + + int numDirectories = 0; + while (offset != 0L) { + ++numDirectories; + + stream.seek(offset); + long entries = readUnsignedShort(stream, isBigEndian); + stream.skip(12 * entries); + offset = readUnsignedInt(stream, isBigEndian); + } + + stream.seek(pointer); // Reset stream pointer + return numDirectories; + } + + /** + * Returns a boolean indicating whether the byte order used in the + * the TIFF file is big-endian. That is, whether the byte order is from + * the most significant to the least significant. + */ + public boolean isBigEndian() { + return isBigEndian; + } + + /** + * Returns the offset of the IFD corresponding to this + * TIFFDirectory. + */ + public long getIFDOffset() { + return ifdOffset; + } + + /** + * Returns the offset of the next IFD after the IFD corresponding to this + * TIFFDirectory. + */ + public long getNextIFDOffset() { + return nextIFDOffset; + } +} diff --git a/src/main/java/org/apache/xmlgraphics/image/codec/tiff/TIFFEncodeParam.java b/src/main/java/org/apache/xmlgraphics/image/codec/tiff/TIFFEncodeParam.java new file mode 100644 index 0000000..1305eb4 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/codec/tiff/TIFFEncodeParam.java @@ -0,0 +1,253 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: TIFFEncodeParam.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.image.codec.tiff; + +import java.util.Iterator; +import java.util.zip.Deflater; + +import org.apache.xmlgraphics.image.codec.util.ImageEncodeParam; +import org.apache.xmlgraphics.image.codec.util.PropertyUtil; + +/** + * An instance of ImageEncodeParam for encoding images in + * the TIFF format. + * + *

    This class allows for the specification of encoding parameters. By + * default, the image is encoded without any compression, and is written + * out consisting of strips, not tiles. The particular compression scheme + * to be used can be specified by using the setCompression() + * method. The compression scheme specified will be honored only if it is + * compatible with the type of image being written out. For example, + * Group3 and Group4 compressions can only be used with Bilevel images. + * Writing of tiled TIFF images can be enabled by calling the + * setWriteTiled() method. + * + *

    This class is not a committed part of the JAI API. It may + * be removed or changed in future releases of JAI. + * + */ +public class TIFFEncodeParam implements ImageEncodeParam { + + private static final long serialVersionUID = 2471949735040024055L; + private CompressionValue compression = CompressionValue.NONE; + + private boolean writeTiled; + private int tileWidth; + private int tileHeight; + + private Iterator extraImages; + + private TIFFField[] extraFields; + + private boolean convertJPEGRGBToYCbCr = true; + + private int deflateLevel = Deflater.DEFAULT_COMPRESSION; + + /** + * Constructs a TIFFEncodeParam object with default values for + * all parameters. + */ + public TIFFEncodeParam() { + //nop + } + + /** Returns the value of the compression parameter. */ + public CompressionValue getCompression() { + return compression; + } + + /** + * Specifies the type of compression to be used. The compression type + * specified will be honored only if it is compatible with the image + * being written out. Currently only PackBits, JPEG, and DEFLATE + * compression schemes are supported. + * + *

    If compression is set to any value but + * COMPRESSION_NONE and the OutputStream + * supplied to the ImageEncoder is not a + * SeekableOutputStream, then the encoder will use either + * a temporary file or a memory cache when compressing the data + * depending on whether the file system is accessible. Compression + * will therefore be more efficient if a SeekableOutputStream + * is supplied. + * + * @param compression The compression type. + */ + public void setCompression(CompressionValue compression) { + + switch(compression) { + case NONE: + case PACKBITS: + case DEFLATE: + // Do nothing. + break; + default: + throw new RuntimeException(PropertyUtil.getString("TIFFEncodeParam0")); + } + + this.compression = compression; + } + + /** + * Returns the value of the writeTiled parameter. + */ + public boolean getWriteTiled() { + return writeTiled; + } + + /** + * If set, the data will be written out in tiled format, instead of + * in strips. + * + * @param writeTiled Specifies whether the image data should be + * wriiten out in tiled format. + */ + public void setWriteTiled(boolean writeTiled) { + this.writeTiled = writeTiled; + } + + /** + * Sets the dimensions of the tiles to be written. If either + * value is non-positive, the encoder will use a default value. + * + *

    If the data are being written as tiles, i.e., + * getWriteTiled() returns true, then the + * default tile dimensions used by the encoder are those of the tiles + * of the image being encoded. + * + *

    If the data are being written as strips, i.e., + * getWriteTiled() returns false, the width + * of each strip is always the width of the image and the default + * number of rows per strip is 8. + * + * @param tileWidth The tile width; ignored if strips are used. + * @param tileHeight The tile height or number of rows per strip. + */ + public void setTileSize(int tileWidth, int tileHeight) { + this.tileWidth = tileWidth; + this.tileHeight = tileHeight; + } + + /** + * Retrieves the tile width set via setTileSize(). + */ + public int getTileWidth() { + return tileWidth; + } + + /** + * Retrieves the tile height set via setTileSize(). + */ + public int getTileHeight() { + return tileHeight; + } + + /** + * Sets an Iterator of additional images to be written + * after the image passed as an argument to the ImageEncoder. + * The methods on the supplied Iterator must only be invoked + * by the ImageEncoder which will exhaust the available + * values unless an error occurs. + * + *

    The value returned by an invocation of next() on the + * Iterator must return either a RenderedImage + * or an Object[] of length 2 wherein the element at index + * zero is a RenderedImage amd the other element is a + * TIFFEncodeParam. If no TIFFEncodeParam is + * supplied in this manner for an additional image, the parameters used + * to create the ImageEncoder will be used. The extra + * image Iterator set on any TIFFEncodeParam + * of an additional image will in all cases be ignored. + */ + public synchronized void setExtraImages(Iterator extraImages) { + this.extraImages = extraImages; + } + + /** + * Returns the additional image Iterator specified via + * setExtraImages() or null if none was + * supplied or if a null value was supplied. + */ + public synchronized Iterator getExtraImages() { + return extraImages; + } + + /** + * Sets the compression level for DEFLATE-compressed data which should + * either be java.util.Deflater.DEFAULT_COMPRESSION or a + * value in the range [1,9] where larger values indicate more compression. + * The default setting is Deflater.DEFAULT_COMPRESSION. This + * setting is ignored if the compression type is not DEFLATE. + */ + public void setDeflateLevel(int deflateLevel) { + if (deflateLevel != Deflater.DEFAULT_COMPRESSION) { + throw new RuntimeException(PropertyUtil.getString("TIFFEncodeParam1")); + } + + this.deflateLevel = deflateLevel; + } + + /** + * Gets the compression level for DEFLATE compression. + */ + public int getDeflateLevel() { + return deflateLevel; + } + + /** + * Sets flag indicating whether to convert RGB data to YCbCr when the + * compression type is JPEG. The default value is true. + * This flag is ignored if the compression type is not JPEG. + */ + public void setJPEGCompressRGBToYCbCr(boolean convertJPEGRGBToYCbCr) { + this.convertJPEGRGBToYCbCr = convertJPEGRGBToYCbCr; + } + + /** + * Whether RGB data will be converted to YCbCr when using JPEG compression. + */ + public boolean getJPEGCompressRGBToYCbCr() { + return convertJPEGRGBToYCbCr; + } + + /** + * Sets an array of extra fields to be written to the TIFF Image File + * Directory (IFD). Fields with tags equal to the tag of any + * automatically generated fields are ignored. No error checking is + * performed with respect to the validity of the field contents or + * the appropriateness of the field for the image being encoded. + * + * @param extraFields An array of extra fields; the parameter is + * copied by reference. + */ + public void setExtraFields(TIFFField[] extraFields) { + this.extraFields = extraFields; + } + + /** + * Returns the value set by setExtraFields(). + */ + public TIFFField[] getExtraFields() { + if (extraFields == null) { + return new TIFFField[0]; + } + return extraFields; + } +} diff --git a/src/main/java/org/apache/xmlgraphics/image/codec/tiff/TIFFFaxDecoder.java b/src/main/java/org/apache/xmlgraphics/image/codec/tiff/TIFFFaxDecoder.java new file mode 100644 index 0000000..a438dbd --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/codec/tiff/TIFFFaxDecoder.java @@ -0,0 +1,1481 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: TIFFFaxDecoder.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.image.codec.tiff; + +import org.apache.xmlgraphics.image.codec.util.PropertyUtil; + +// CSOFF: InnerAssignment +// CSOFF: MultipleVariableDeclarations +// CSOFF: OperatorWrap +// CSOFF: WhitespaceAround + +class TIFFFaxDecoder { + + private int bitPointer; + private int bytePointer; + private byte[] data; + private int w; + private int fillOrder; + + // Data structures needed to store changing elements for the previous + // and the current scanline + private int changingElemSize; + private int[] prevChangingElems; + private int[] currChangingElems; + + // Element at which to start search in getNextChangingElement + private int lastChangingElement; + + private int compression = 2; + + // Variables set by T4Options +// private int uncompressedMode = 0; + private int fillBits; + private int oneD; + + static int[] table1 = { + 0x00, // 0 bits are left in first byte - SHOULD NOT HAPPEN + 0x01, // 1 bits are left in first byte + 0x03, // 2 bits are left in first byte + 0x07, // 3 bits are left in first byte + 0x0f, // 4 bits are left in first byte + 0x1f, // 5 bits are left in first byte + 0x3f, // 6 bits are left in first byte + 0x7f, // 7 bits are left in first byte + 0xff // 8 bits are left in first byte + }; + + static int[] table2 = { + 0x00, // 0 + 0x80, // 1 + 0xc0, // 2 + 0xe0, // 3 + 0xf0, // 4 + 0xf8, // 5 + 0xfc, // 6 + 0xfe, // 7 + 0xff // 8 + }; + + // Table to be used when fillOrder = 2, for flipping bytes. + static byte[] flipTable = { + 0, -128, 64, -64, 32, -96, 96, -32, + 16, -112, 80, -48, 48, -80, 112, -16, + 8, -120, 72, -56, 40, -88, 104, -24, + 24, -104, 88, -40, 56, -72, 120, -8, + 4, -124, 68, -60, 36, -92, 100, -28, + 20, -108, 84, -44, 52, -76, 116, -12, + 12, -116, 76, -52, 44, -84, 108, -20, + 28, -100, 92, -36, 60, -68, 124, -4, + 2, -126, 66, -62, 34, -94, 98, -30, + 18, -110, 82, -46, 50, -78, 114, -14, + 10, -118, 74, -54, 42, -86, 106, -22, + 26, -102, 90, -38, 58, -70, 122, -6, + 6, -122, 70, -58, 38, -90, 102, -26, + 22, -106, 86, -42, 54, -74, 118, -10, + 14, -114, 78, -50, 46, -82, 110, -18, + 30, -98, 94, -34, 62, -66, 126, -2, + 1, -127, 65, -63, 33, -95, 97, -31, + 17, -111, 81, -47, 49, -79, 113, -15, + 9, -119, 73, -55, 41, -87, 105, -23, + 25, -103, 89, -39, 57, -71, 121, -7, + 5, -123, 69, -59, 37, -91, 101, -27, + 21, -107, 85, -43, 53, -75, 117, -11, + 13, -115, 77, -51, 45, -83, 109, -19, + 29, -99, 93, -35, 61, -67, 125, -3, + 3, -125, 67, -61, 35, -93, 99, -29, + 19, -109, 83, -45, 51, -77, 115, -13, + 11, -117, 75, -53, 43, -85, 107, -21, + 27, -101, 91, -37, 59, -69, 123, -5, + 7, -121, 71, -57, 39, -89, 103, -25, + 23, -105, 87, -41, 55, -73, 119, -9, + 15, -113, 79, -49, 47, -81, 111, -17, + 31, -97, 95, -33, 63, -65, 127, -1, + }; + + // The main 10 bit white runs lookup table + static short[] white = { + // 0 - 7 + 6430, 6400, 6400, 6400, 3225, 3225, 3225, 3225, + // 8 - 15 + 944, 944, 944, 944, 976, 976, 976, 976, + // 16 - 23 + 1456, 1456, 1456, 1456, 1488, 1488, 1488, 1488, + // 24 - 31 + 718, 718, 718, 718, 718, 718, 718, 718, + // 32 - 39 + 750, 750, 750, 750, 750, 750, 750, 750, + // 40 - 47 + 1520, 1520, 1520, 1520, 1552, 1552, 1552, 1552, + // 48 - 55 + 428, 428, 428, 428, 428, 428, 428, 428, + // 56 - 63 + 428, 428, 428, 428, 428, 428, 428, 428, + // 64 - 71 + 654, 654, 654, 654, 654, 654, 654, 654, + // 72 - 79 + 1072, 1072, 1072, 1072, 1104, 1104, 1104, 1104, + // 80 - 87 + 1136, 1136, 1136, 1136, 1168, 1168, 1168, 1168, + // 88 - 95 + 1200, 1200, 1200, 1200, 1232, 1232, 1232, 1232, + // 96 - 103 + 622, 622, 622, 622, 622, 622, 622, 622, + // 104 - 111 + 1008, 1008, 1008, 1008, 1040, 1040, 1040, 1040, + // 112 - 119 + 44, 44, 44, 44, 44, 44, 44, 44, + // 120 - 127 + 44, 44, 44, 44, 44, 44, 44, 44, + // 128 - 135 + 396, 396, 396, 396, 396, 396, 396, 396, + // 136 - 143 + 396, 396, 396, 396, 396, 396, 396, 396, + // 144 - 151 + 1712, 1712, 1712, 1712, 1744, 1744, 1744, 1744, + // 152 - 159 + 846, 846, 846, 846, 846, 846, 846, 846, + // 160 - 167 + 1264, 1264, 1264, 1264, 1296, 1296, 1296, 1296, + // 168 - 175 + 1328, 1328, 1328, 1328, 1360, 1360, 1360, 1360, + // 176 - 183 + 1392, 1392, 1392, 1392, 1424, 1424, 1424, 1424, + // 184 - 191 + 686, 686, 686, 686, 686, 686, 686, 686, + // 192 - 199 + 910, 910, 910, 910, 910, 910, 910, 910, + // 200 - 207 + 1968, 1968, 1968, 1968, 2000, 2000, 2000, 2000, + // 208 - 215 + 2032, 2032, 2032, 2032, 16, 16, 16, 16, + // 216 - 223 + 10257, 10257, 10257, 10257, 12305, 12305, 12305, 12305, + // 224 - 231 + 330, 330, 330, 330, 330, 330, 330, 330, + // 232 - 239 + 330, 330, 330, 330, 330, 330, 330, 330, + // 240 - 247 + 330, 330, 330, 330, 330, 330, 330, 330, + // 248 - 255 + 330, 330, 330, 330, 330, 330, 330, 330, + // 256 - 263 + 362, 362, 362, 362, 362, 362, 362, 362, + // 264 - 271 + 362, 362, 362, 362, 362, 362, 362, 362, + // 272 - 279 + 362, 362, 362, 362, 362, 362, 362, 362, + // 280 - 287 + 362, 362, 362, 362, 362, 362, 362, 362, + // 288 - 295 + 878, 878, 878, 878, 878, 878, 878, 878, + // 296 - 303 + 1904, 1904, 1904, 1904, 1936, 1936, 1936, 1936, + // 304 - 311 + -18413, -18413, -16365, -16365, -14317, -14317, -10221, -10221, + // 312 - 319 + 590, 590, 590, 590, 590, 590, 590, 590, + // 320 - 327 + 782, 782, 782, 782, 782, 782, 782, 782, + // 328 - 335 + 1584, 1584, 1584, 1584, 1616, 1616, 1616, 1616, + // 336 - 343 + 1648, 1648, 1648, 1648, 1680, 1680, 1680, 1680, + // 344 - 351 + 814, 814, 814, 814, 814, 814, 814, 814, + // 352 - 359 + 1776, 1776, 1776, 1776, 1808, 1808, 1808, 1808, + // 360 - 367 + 1840, 1840, 1840, 1840, 1872, 1872, 1872, 1872, + // 368 - 375 + 6157, 6157, 6157, 6157, 6157, 6157, 6157, 6157, + // 376 - 383 + 6157, 6157, 6157, 6157, 6157, 6157, 6157, 6157, + // 384 - 391 + -12275, -12275, -12275, -12275, -12275, -12275, -12275, -12275, + // 392 - 399 + -12275, -12275, -12275, -12275, -12275, -12275, -12275, -12275, + // 400 - 407 + 14353, 14353, 14353, 14353, 16401, 16401, 16401, 16401, + // 408 - 415 + 22547, 22547, 24595, 24595, 20497, 20497, 20497, 20497, + // 416 - 423 + 18449, 18449, 18449, 18449, 26643, 26643, 28691, 28691, + // 424 - 431 + 30739, 30739, -32749, -32749, -30701, -30701, -28653, -28653, + // 432 - 439 + -26605, -26605, -24557, -24557, -22509, -22509, -20461, -20461, + // 440 - 447 + 8207, 8207, 8207, 8207, 8207, 8207, 8207, 8207, + // 448 - 455 + 72, 72, 72, 72, 72, 72, 72, 72, + // 456 - 463 + 72, 72, 72, 72, 72, 72, 72, 72, + // 464 - 471 + 72, 72, 72, 72, 72, 72, 72, 72, + // 472 - 479 + 72, 72, 72, 72, 72, 72, 72, 72, + // 480 - 487 + 72, 72, 72, 72, 72, 72, 72, 72, + // 488 - 495 + 72, 72, 72, 72, 72, 72, 72, 72, + // 496 - 503 + 72, 72, 72, 72, 72, 72, 72, 72, + // 504 - 511 + 72, 72, 72, 72, 72, 72, 72, 72, + // 512 - 519 + 104, 104, 104, 104, 104, 104, 104, 104, + // 520 - 527 + 104, 104, 104, 104, 104, 104, 104, 104, + // 528 - 535 + 104, 104, 104, 104, 104, 104, 104, 104, + // 536 - 543 + 104, 104, 104, 104, 104, 104, 104, 104, + // 544 - 551 + 104, 104, 104, 104, 104, 104, 104, 104, + // 552 - 559 + 104, 104, 104, 104, 104, 104, 104, 104, + // 560 - 567 + 104, 104, 104, 104, 104, 104, 104, 104, + // 568 - 575 + 104, 104, 104, 104, 104, 104, 104, 104, + // 576 - 583 + 4107, 4107, 4107, 4107, 4107, 4107, 4107, 4107, + // 584 - 591 + 4107, 4107, 4107, 4107, 4107, 4107, 4107, 4107, + // 592 - 599 + 4107, 4107, 4107, 4107, 4107, 4107, 4107, 4107, + // 600 - 607 + 4107, 4107, 4107, 4107, 4107, 4107, 4107, 4107, + // 608 - 615 + 266, 266, 266, 266, 266, 266, 266, 266, + // 616 - 623 + 266, 266, 266, 266, 266, 266, 266, 266, + // 624 - 631 + 266, 266, 266, 266, 266, 266, 266, 266, + // 632 - 639 + 266, 266, 266, 266, 266, 266, 266, 266, + // 640 - 647 + 298, 298, 298, 298, 298, 298, 298, 298, + // 648 - 655 + 298, 298, 298, 298, 298, 298, 298, 298, + // 656 - 663 + 298, 298, 298, 298, 298, 298, 298, 298, + // 664 - 671 + 298, 298, 298, 298, 298, 298, 298, 298, + // 672 - 679 + 524, 524, 524, 524, 524, 524, 524, 524, + // 680 - 687 + 524, 524, 524, 524, 524, 524, 524, 524, + // 688 - 695 + 556, 556, 556, 556, 556, 556, 556, 556, + // 696 - 703 + 556, 556, 556, 556, 556, 556, 556, 556, + // 704 - 711 + 136, 136, 136, 136, 136, 136, 136, 136, + // 712 - 719 + 136, 136, 136, 136, 136, 136, 136, 136, + // 720 - 727 + 136, 136, 136, 136, 136, 136, 136, 136, + // 728 - 735 + 136, 136, 136, 136, 136, 136, 136, 136, + // 736 - 743 + 136, 136, 136, 136, 136, 136, 136, 136, + // 744 - 751 + 136, 136, 136, 136, 136, 136, 136, 136, + // 752 - 759 + 136, 136, 136, 136, 136, 136, 136, 136, + // 760 - 767 + 136, 136, 136, 136, 136, 136, 136, 136, + // 768 - 775 + 168, 168, 168, 168, 168, 168, 168, 168, + // 776 - 783 + 168, 168, 168, 168, 168, 168, 168, 168, + // 784 - 791 + 168, 168, 168, 168, 168, 168, 168, 168, + // 792 - 799 + 168, 168, 168, 168, 168, 168, 168, 168, + // 800 - 807 + 168, 168, 168, 168, 168, 168, 168, 168, + // 808 - 815 + 168, 168, 168, 168, 168, 168, 168, 168, + // 816 - 823 + 168, 168, 168, 168, 168, 168, 168, 168, + // 824 - 831 + 168, 168, 168, 168, 168, 168, 168, 168, + // 832 - 839 + 460, 460, 460, 460, 460, 460, 460, 460, + // 840 - 847 + 460, 460, 460, 460, 460, 460, 460, 460, + // 848 - 855 + 492, 492, 492, 492, 492, 492, 492, 492, + // 856 - 863 + 492, 492, 492, 492, 492, 492, 492, 492, + // 864 - 871 + 2059, 2059, 2059, 2059, 2059, 2059, 2059, 2059, + // 872 - 879 + 2059, 2059, 2059, 2059, 2059, 2059, 2059, 2059, + // 880 - 887 + 2059, 2059, 2059, 2059, 2059, 2059, 2059, 2059, + // 888 - 895 + 2059, 2059, 2059, 2059, 2059, 2059, 2059, 2059, + // 896 - 903 + 200, 200, 200, 200, 200, 200, 200, 200, + // 904 - 911 + 200, 200, 200, 200, 200, 200, 200, 200, + // 912 - 919 + 200, 200, 200, 200, 200, 200, 200, 200, + // 920 - 927 + 200, 200, 200, 200, 200, 200, 200, 200, + // 928 - 935 + 200, 200, 200, 200, 200, 200, 200, 200, + // 936 - 943 + 200, 200, 200, 200, 200, 200, 200, 200, + // 944 - 951 + 200, 200, 200, 200, 200, 200, 200, 200, + // 952 - 959 + 200, 200, 200, 200, 200, 200, 200, 200, + // 960 - 967 + 232, 232, 232, 232, 232, 232, 232, 232, + // 968 - 975 + 232, 232, 232, 232, 232, 232, 232, 232, + // 976 - 983 + 232, 232, 232, 232, 232, 232, 232, 232, + // 984 - 991 + 232, 232, 232, 232, 232, 232, 232, 232, + // 992 - 999 + 232, 232, 232, 232, 232, 232, 232, 232, + // 1000 - 1007 + 232, 232, 232, 232, 232, 232, 232, 232, + // 1008 - 1015 + 232, 232, 232, 232, 232, 232, 232, 232, + // 1016 - 1023 + 232, 232, 232, 232, 232, 232, 232, 232, + }; + + // Additional make up codes for both White and Black runs + static short[] additionalMakeup = { + 28679, 28679, 31752, (short)32777, + (short)33801, (short)34825, (short)35849, (short)36873, + (short)29703, (short)29703, (short)30727, (short)30727, + (short)37897, (short)38921, (short)39945, (short)40969 + }; + + // Initial black run look up table, uses the first 4 bits of a code + static short[] initBlack = { + // 0 - 7 + 3226, 6412, 200, 168, 38, 38, 134, 134, + // 8 - 15 + 100, 100, 100, 100, 68, 68, 68, 68 + }; + + // + static short[] twoBitBlack = {292, 260, 226, 226}; // 0 - 3 + + // Main black run table, using the last 9 bits of possible 13 bit code + static short[] black = { + // 0 - 7 + 62, 62, 30, 30, 0, 0, 0, 0, + // 8 - 15 + 0, 0, 0, 0, 0, 0, 0, 0, + // 16 - 23 + 0, 0, 0, 0, 0, 0, 0, 0, + // 24 - 31 + 0, 0, 0, 0, 0, 0, 0, 0, + // 32 - 39 + 3225, 3225, 3225, 3225, 3225, 3225, 3225, 3225, + // 40 - 47 + 3225, 3225, 3225, 3225, 3225, 3225, 3225, 3225, + // 48 - 55 + 3225, 3225, 3225, 3225, 3225, 3225, 3225, 3225, + // 56 - 63 + 3225, 3225, 3225, 3225, 3225, 3225, 3225, 3225, + // 64 - 71 + 588, 588, 588, 588, 588, 588, 588, 588, + // 72 - 79 + 1680, 1680, 20499, 22547, 24595, 26643, 1776, 1776, + // 80 - 87 + 1808, 1808, -24557, -22509, -20461, -18413, 1904, 1904, + // 88 - 95 + 1936, 1936, -16365, -14317, 782, 782, 782, 782, + // 96 - 103 + 814, 814, 814, 814, -12269, -10221, 10257, 10257, + // 104 - 111 + 12305, 12305, 14353, 14353, 16403, 18451, 1712, 1712, + // 112 - 119 + 1744, 1744, 28691, 30739, -32749, -30701, -28653, -26605, + // 120 - 127 + 2061, 2061, 2061, 2061, 2061, 2061, 2061, 2061, + // 128 - 135 + 424, 424, 424, 424, 424, 424, 424, 424, + // 136 - 143 + 424, 424, 424, 424, 424, 424, 424, 424, + // 144 - 151 + 424, 424, 424, 424, 424, 424, 424, 424, + // 152 - 159 + 424, 424, 424, 424, 424, 424, 424, 424, + // 160 - 167 + 750, 750, 750, 750, 1616, 1616, 1648, 1648, + // 168 - 175 + 1424, 1424, 1456, 1456, 1488, 1488, 1520, 1520, + // 176 - 183 + 1840, 1840, 1872, 1872, 1968, 1968, 8209, 8209, + // 184 - 191 + 524, 524, 524, 524, 524, 524, 524, 524, + // 192 - 199 + 556, 556, 556, 556, 556, 556, 556, 556, + // 200 - 207 + 1552, 1552, 1584, 1584, 2000, 2000, 2032, 2032, + // 208 - 215 + 976, 976, 1008, 1008, 1040, 1040, 1072, 1072, + // 216 - 223 + 1296, 1296, 1328, 1328, 718, 718, 718, 718, + // 224 - 231 + 456, 456, 456, 456, 456, 456, 456, 456, + // 232 - 239 + 456, 456, 456, 456, 456, 456, 456, 456, + // 240 - 247 + 456, 456, 456, 456, 456, 456, 456, 456, + // 248 - 255 + 456, 456, 456, 456, 456, 456, 456, 456, + // 256 - 263 + 326, 326, 326, 326, 326, 326, 326, 326, + // 264 - 271 + 326, 326, 326, 326, 326, 326, 326, 326, + // 272 - 279 + 326, 326, 326, 326, 326, 326, 326, 326, + // 280 - 287 + 326, 326, 326, 326, 326, 326, 326, 326, + // 288 - 295 + 326, 326, 326, 326, 326, 326, 326, 326, + // 296 - 303 + 326, 326, 326, 326, 326, 326, 326, 326, + // 304 - 311 + 326, 326, 326, 326, 326, 326, 326, 326, + // 312 - 319 + 326, 326, 326, 326, 326, 326, 326, 326, + // 320 - 327 + 358, 358, 358, 358, 358, 358, 358, 358, + // 328 - 335 + 358, 358, 358, 358, 358, 358, 358, 358, + // 336 - 343 + 358, 358, 358, 358, 358, 358, 358, 358, + // 344 - 351 + 358, 358, 358, 358, 358, 358, 358, 358, + // 352 - 359 + 358, 358, 358, 358, 358, 358, 358, 358, + // 360 - 367 + 358, 358, 358, 358, 358, 358, 358, 358, + // 368 - 375 + 358, 358, 358, 358, 358, 358, 358, 358, + // 376 - 383 + 358, 358, 358, 358, 358, 358, 358, 358, + // 384 - 391 + 490, 490, 490, 490, 490, 490, 490, 490, + // 392 - 399 + 490, 490, 490, 490, 490, 490, 490, 490, + // 400 - 407 + 4113, 4113, 6161, 6161, 848, 848, 880, 880, + // 408 - 415 + 912, 912, 944, 944, 622, 622, 622, 622, + // 416 - 423 + 654, 654, 654, 654, 1104, 1104, 1136, 1136, + // 424 - 431 + 1168, 1168, 1200, 1200, 1232, 1232, 1264, 1264, + // 432 - 439 + 686, 686, 686, 686, 1360, 1360, 1392, 1392, + // 440 - 447 + 12, 12, 12, 12, 12, 12, 12, 12, + // 448 - 455 + 390, 390, 390, 390, 390, 390, 390, 390, + // 456 - 463 + 390, 390, 390, 390, 390, 390, 390, 390, + // 464 - 471 + 390, 390, 390, 390, 390, 390, 390, 390, + // 472 - 479 + 390, 390, 390, 390, 390, 390, 390, 390, + // 480 - 487 + 390, 390, 390, 390, 390, 390, 390, 390, + // 488 - 495 + 390, 390, 390, 390, 390, 390, 390, 390, + // 496 - 503 + 390, 390, 390, 390, 390, 390, 390, 390, + // 504 - 511 + 390, 390, 390, 390, 390, 390, 390, 390, + }; + + static byte[] twoDCodes = { + // 0 - 7 + 80, 88, 23, 71, 30, 30, 62, 62, + // 8 - 15 + 4, 4, 4, 4, 4, 4, 4, 4, + // 16 - 23 + 11, 11, 11, 11, 11, 11, 11, 11, + // 24 - 31 + 11, 11, 11, 11, 11, 11, 11, 11, + // 32 - 39 + 35, 35, 35, 35, 35, 35, 35, 35, + // 40 - 47 + 35, 35, 35, 35, 35, 35, 35, 35, + // 48 - 55 + 51, 51, 51, 51, 51, 51, 51, 51, + // 56 - 63 + 51, 51, 51, 51, 51, 51, 51, 51, + // 64 - 71 + 41, 41, 41, 41, 41, 41, 41, 41, + // 72 - 79 + 41, 41, 41, 41, 41, 41, 41, 41, + // 80 - 87 + 41, 41, 41, 41, 41, 41, 41, 41, + // 88 - 95 + 41, 41, 41, 41, 41, 41, 41, 41, + // 96 - 103 + 41, 41, 41, 41, 41, 41, 41, 41, + // 104 - 111 + 41, 41, 41, 41, 41, 41, 41, 41, + // 112 - 119 + 41, 41, 41, 41, 41, 41, 41, 41, + // 120 - 127 + 41, 41, 41, 41, 41, 41, 41, 41, + }; + + /** + * @param fillOrder The fill order of the compressed data bytes. + * @param w The width of the image in pixels + * @param h The height of the image in pixels + */ + public TIFFFaxDecoder(int fillOrder, int w, int h) { + this.fillOrder = fillOrder; + this.w = w; +// this.h = h; + + this.bitPointer = 0; + this.bytePointer = 0; + this.prevChangingElems = new int[w]; + this.currChangingElems = new int[w]; + } + + // One-dimensional decoding methods + + public void decode1D(byte[] buffer, byte[] compData, + int startX, int height) { + this.data = compData; + + int lineOffset = 0; + int scanlineStride = (w + 7) / 8; + + bitPointer = 0; + bytePointer = 0; + + for (int i = 0; i < height; i++) { + decodeNextScanline(buffer, lineOffset, startX); + lineOffset += scanlineStride; + } + } + + public void decodeNextScanline(byte[] buffer, + int lineOffset, int bitOffset) { + int bits = 0; + int code = 0; + int isT = 0; + int current; + int entry; + int twoBits; + boolean isWhite = true; + + // Initialize starting of the changing elements array + changingElemSize = 0; + + // While scanline not complete + while (bitOffset < w) { + while (isWhite) { + // White run + current = nextNBits(10); + entry = white[current]; + + // Get the 3 fields from the entry + isT = entry & 0x0001; + bits = (entry >>> 1) & 0x0f; + + if (bits == 12) { // Additional Make up code + // Get the next 2 bits + twoBits = nextLesserThan8Bits(2); + // Consolidate the 2 new bits and last 2 bits into 4 bits + current = ((current << 2) & 0x000c) | twoBits; + entry = additionalMakeup[current]; + bits = (entry >>> 1) & 0x07; // 3 bits 0000 0111 + code = (entry >>> 4) & 0x0fff; // 12 bits + bitOffset += code; // Skip white run + + updatePointer(4 - bits); + } else if (bits == 0) { // ERROR + throw new RuntimeException(PropertyUtil.getString("TIFFFaxDecoder0")); + } else if (bits == 15) { // EOL + throw new RuntimeException(PropertyUtil.getString("TIFFFaxDecoder1")); + } else { + // 11 bits - 0000 0111 1111 1111 = 0x07ff + code = (entry >>> 5) & 0x07ff; + bitOffset += code; + + updatePointer(10 - bits); + if (isT == 0) { + isWhite = false; + currChangingElems[changingElemSize++] = bitOffset; + } + } + } + + // Check whether this run completed one width, if so + // advance to next byte boundary for compression = 2. + if (bitOffset == w) { + if (compression == 2) { + advancePointer(); + } + break; + } + + while (!isWhite) { + // Black run + current = nextLesserThan8Bits(4); + entry = initBlack[current]; + + // Get the 3 fields from the entry + isT = entry & 0x0001; + bits = (entry >>> 1) & 0x000f; + code = (entry >>> 5) & 0x07ff; + + if (code == 100) { + current = nextNBits(9); + entry = black[current]; + + // Get the 3 fields from the entry + isT = entry & 0x0001; + bits = (entry >>> 1) & 0x000f; + code = (entry >>> 5) & 0x07ff; + + if (bits == 12) { + // Additional makeup codes + updatePointer(5); + current = nextLesserThan8Bits(4); + entry = additionalMakeup[current]; + bits = (entry >>> 1) & 0x07; // 3 bits 0000 0111 + code = (entry >>> 4) & 0x0fff; // 12 bits + + setToBlack(buffer, lineOffset, bitOffset, code); + bitOffset += code; + + updatePointer(4 - bits); + } else if (bits == 15) { + // EOL code + throw new RuntimeException(PropertyUtil.getString("TIFFFaxDecoder2")); + } else { + setToBlack(buffer, lineOffset, bitOffset, code); + bitOffset += code; + + updatePointer(9 - bits); + if (isT == 0) { + isWhite = true; + currChangingElems[changingElemSize++] = bitOffset; + } + } + } else if (code == 200) { + // Is a Terminating code + current = nextLesserThan8Bits(2); + entry = twoBitBlack[current]; + code = (entry >>> 5) & 0x07ff; + bits = (entry >>> 1) & 0x0f; + + setToBlack(buffer, lineOffset, bitOffset, code); + bitOffset += code; + + updatePointer(2 - bits); + isWhite = true; + currChangingElems[changingElemSize++] = bitOffset; + } else { + // Is a Terminating code + setToBlack(buffer, lineOffset, bitOffset, code); + bitOffset += code; + + updatePointer(4 - bits); + isWhite = true; + currChangingElems[changingElemSize++] = bitOffset; + } + } + + // Check whether this run completed one width + if (bitOffset == w) { + if (compression == 2) { + advancePointer(); + } + break; + } + } + + currChangingElems[changingElemSize++] = bitOffset; + } + + // Two-dimensional decoding methods + + public void decode2D(byte[] buffer, + byte[] compData, + int startX, + int height, + long tiffT4Options) { + this.data = compData; + compression = 3; + + bitPointer = 0; + bytePointer = 0; + + int scanlineStride = (w + 7) / 8; + + int a0; + int a1; + int b1; + int b2; + int[] b = new int[2]; + int entry; + int code; + int bits; + boolean isWhite; + int currIndex = 0; + int[] temp; + + // fillBits - dealt with this in readEOL + // 1D/2D encoding - dealt with this in readEOL + + // uncompressedMode - haven't dealt with this yet. + + + oneD = (int)(tiffT4Options & 0x01); +// uncompressedMode = (int)((tiffT4Options & 0x02) >> 1); + fillBits = (int)((tiffT4Options & 0x04) >> 2); + + // The data must start with an EOL code + if (readEOL() != 1) { + throw new RuntimeException(PropertyUtil.getString("TIFFFaxDecoder3")); + } + + int lineOffset = 0; + int bitOffset; + + // Then the 1D encoded scanline data will occur, changing elements + // array gets set. + decodeNextScanline(buffer, lineOffset, startX); + lineOffset += scanlineStride; + + for (int lines = 1; lines < height; lines++) { + + // Every line must begin with an EOL followed by a bit which + // indicates whether the following scanline is 1D or 2D encoded. + if (readEOL() == 0) { + // 2D encoded scanline follows + + // Initialize previous scanlines changing elements, and + // initialize current scanline's changing elements array + temp = prevChangingElems; + prevChangingElems = currChangingElems; + currChangingElems = temp; + currIndex = 0; + + // a0 has to be set just before the start of this scanline. + a0 = -1; + isWhite = true; + bitOffset = startX; + + lastChangingElement = 0; + + while (bitOffset < w) { + // Get the next changing element + getNextChangingElement(a0, isWhite, b); + + b1 = b[0]; + b2 = b[1]; + + // Get the next seven bits + entry = nextLesserThan8Bits(7); + + // Run these through the 2DCodes table + entry = (int)(twoDCodes[entry] & 0xff); + + // Get the code and the number of bits used up + code = (entry & 0x78) >>> 3; + bits = entry & 0x07; + + if (code == 0) { + if (!isWhite) { + setToBlack(buffer, lineOffset, bitOffset, + b2 - bitOffset); + } + bitOffset = a0 = b2; + + // Set pointer to consume the correct number of bits. + updatePointer(7 - bits); + } else if (code == 1) { + // Horizontal + updatePointer(7 - bits); + + // identify the next 2 codes. + int number; + if (isWhite) { + number = decodeWhiteCodeWord(); + bitOffset += number; + currChangingElems[currIndex++] = bitOffset; + + number = decodeBlackCodeWord(); + setToBlack(buffer, lineOffset, bitOffset, number); + bitOffset += number; + currChangingElems[currIndex++] = bitOffset; + } else { + number = decodeBlackCodeWord(); + setToBlack(buffer, lineOffset, bitOffset, number); + bitOffset += number; + currChangingElems[currIndex++] = bitOffset; + + number = decodeWhiteCodeWord(); + bitOffset += number; + currChangingElems[currIndex++] = bitOffset; + } + + a0 = bitOffset; + } else if (code <= 8) { + // Vertical + a1 = b1 + (code - 5); + + currChangingElems[currIndex++] = a1; + + // We write the current color till a1 - 1 pos, + // since a1 is where the next color starts + if (!isWhite) { + setToBlack(buffer, lineOffset, bitOffset, + a1 - bitOffset); + } + bitOffset = a0 = a1; + isWhite = !isWhite; + + updatePointer(7 - bits); + } else { + throw new RuntimeException(PropertyUtil.getString("TIFFFaxDecoder4")); + } + } + + // Add the changing element beyond the current scanline for the + // other color too + currChangingElems[currIndex++] = bitOffset; + changingElemSize = currIndex; + } else { + // 1D encoded scanline follows + decodeNextScanline(buffer, lineOffset, startX); + } + + lineOffset += scanlineStride; + } + } + + public synchronized void decodeT6(byte[] buffer, + byte[] compData, + int startX, + int height, + long tiffT6Options) { + this.data = compData; + compression = 4; + + bitPointer = 0; + bytePointer = 0; + + int scanlineStride = (w + 7) / 8; + + int a0; + int a1; + int b1; + int b2; + int entry; + int code; + int bits; + boolean isWhite; + int currIndex; + int[] temp; + + // Return values from getNextChangingElement + int[] b = new int[2]; + + // uncompressedMode - have written some code for this, but this + // has not been tested due to lack of test images using this optional + +// uncompressedMode = (int)((tiffT6Options & 0x02) >> 1); + + // Local cached reference + int[] cce = currChangingElems; + + // Assume invisible preceding row of all white pixels and insert + // both black and white changing elements beyond the end of this + // imaginary scanline. + changingElemSize = 0; + cce[changingElemSize++] = w; + cce[changingElemSize++] = w; + + int lineOffset = 0; + int bitOffset; + + for (int lines = 0; lines < height; lines++) { + // a0 has to be set just before the start of the scanline. + a0 = -1; + isWhite = true; + + // Assign the changing elements of the previous scanline to + // prevChangingElems and start putting this new scanline's + // changing elements into the currChangingElems. + temp = prevChangingElems; + prevChangingElems = currChangingElems; + cce = currChangingElems = temp; + currIndex = 0; + + // Start decoding the scanline at startX in the raster + bitOffset = startX; + + // Reset search start position for getNextChangingElement + lastChangingElement = 0; + + // Till one whole scanline is decoded + while (bitOffset < w) { + // Get the next changing element + getNextChangingElement(a0, isWhite, b); + b1 = b[0]; + b2 = b[1]; + + // Get the next seven bits + entry = nextLesserThan8Bits(7); + // Run these through the 2DCodes table + entry = (int)(twoDCodes[entry] & 0xff); + + // Get the code and the number of bits used up + code = (entry & 0x78) >>> 3; + bits = entry & 0x07; + + if (code == 0) { // Pass + // We always assume WhiteIsZero format for fax. + if (!isWhite) { + setToBlack(buffer, lineOffset, bitOffset, + b2 - bitOffset); + } + bitOffset = a0 = b2; + + // Set pointer to only consume the correct number of bits. + updatePointer(7 - bits); + } else if (code == 1) { // Horizontal + // Set pointer to only consume the correct number of bits. + updatePointer(7 - bits); + + // identify the next 2 alternating color codes. + int number; + if (isWhite) { + // Following are white and black runs + number = decodeWhiteCodeWord(); + bitOffset += number; + cce[currIndex++] = bitOffset; + + number = decodeBlackCodeWord(); + setToBlack(buffer, lineOffset, bitOffset, number); + bitOffset += number; + cce[currIndex++] = bitOffset; + } else { + // First a black run and then a white run follows + number = decodeBlackCodeWord(); + setToBlack(buffer, lineOffset, bitOffset, number); + bitOffset += number; + cce[currIndex++] = bitOffset; + + number = decodeWhiteCodeWord(); + bitOffset += number; + cce[currIndex++] = bitOffset; + } + + a0 = bitOffset; + } else if (code <= 8) { // Vertical + a1 = b1 + (code - 5); + cce[currIndex++] = a1; + + // We write the current color till a1 - 1 pos, + // since a1 is where the next color starts + if (!isWhite) { + setToBlack(buffer, lineOffset, bitOffset, + a1 - bitOffset); + } + bitOffset = a0 = a1; + isWhite = !isWhite; + + updatePointer(7 - bits); + } else if (code == 11) { + if (nextLesserThan8Bits(3) != 7) { + throw new RuntimeException(PropertyUtil.getString("TIFFFaxDecoder5")); + } + + int zeros = 0; + boolean exit = false; + + while (!exit) { + while (nextLesserThan8Bits(1) != 1) { + zeros++; + } + + if (zeros > 5) { + // Exit code + + // Zeros before exit code + zeros = zeros - 6; + + if (!isWhite && (zeros > 0)) { + cce[currIndex++] = bitOffset; + } + + // Zeros before the exit code + bitOffset += zeros; + if (zeros > 0) { + // Some zeros have been written + isWhite = true; + } + + // Read in the bit which specifies the color of + // the following run + if (nextLesserThan8Bits(1) == 0) { + if (!isWhite) { + cce[currIndex++] = bitOffset; + } + isWhite = true; + } else { + if (isWhite) { + cce[currIndex++] = bitOffset; + } + isWhite = false; + } + + exit = true; + } + + if (zeros == 5) { + if (!isWhite) { + cce[currIndex++] = bitOffset; + } + bitOffset += zeros; + + // Last thing written was white + isWhite = true; + } else { + bitOffset += zeros; + + cce[currIndex++] = bitOffset; + setToBlack(buffer, lineOffset, bitOffset, 1); + ++bitOffset; + + // Last thing written was black + isWhite = false; + } + + } + } else { + throw new RuntimeException(PropertyUtil.getString("TIFFFaxDecoder5")); + } + } + + // Add the changing element beyond the current scanline for the + // other color too + cce[currIndex++] = bitOffset; + + // Number of changing elements in this scanline. + changingElemSize = currIndex; + + lineOffset += scanlineStride; + } + } + + private void setToBlack(byte[] buffer, + int lineOffset, int bitOffset, + int numBits) { + int bitNum = 8 * lineOffset + bitOffset; + int lastBit = bitNum + numBits; + + int byteNum = bitNum >> 3; + + // Handle bits in first byte + int shift = bitNum & 0x7; + if (shift > 0) { + int maskVal = 1 << (7 - shift); + byte val = buffer[byteNum]; + while (maskVal > 0 && bitNum < lastBit) { + val |= maskVal; + maskVal >>= 1; + ++bitNum; + } + buffer[byteNum] = val; + } + + // Fill in 8 bits at a time + byteNum = bitNum >> 3; + while (bitNum < lastBit - 7) { + buffer[byteNum++] = (byte)255; + bitNum += 8; + } + + // Fill in remaining bits + while (bitNum < lastBit) { + byteNum = bitNum >> 3; + buffer[byteNum] |= 1 << (7 - (bitNum & 0x7)); + ++bitNum; + } + } + + // Returns run length + private int decodeWhiteCodeWord() { + int current; + int entry; + int bits; + int isT; + int twoBits; + int code = -1; + int runLength = 0; + boolean isWhite = true; + + while (isWhite) { + current = nextNBits(10); + entry = white[current]; + + // Get the 3 fields from the entry + isT = entry & 0x0001; + bits = (entry >>> 1) & 0x0f; + + if (bits == 12) { // Additional Make up code + // Get the next 2 bits + twoBits = nextLesserThan8Bits(2); + // Consolidate the 2 new bits and last 2 bits into 4 bits + current = ((current << 2) & 0x000c) | twoBits; + entry = additionalMakeup[current]; + bits = (entry >>> 1) & 0x07; // 3 bits 0000 0111 + code = (entry >>> 4) & 0x0fff; // 12 bits + runLength += code; + updatePointer(4 - bits); + } else if (bits == 0) { // ERROR + throw new RuntimeException(PropertyUtil.getString("TIFFFaxDecoder0")); + } else if (bits == 15) { // EOL + throw new RuntimeException(PropertyUtil.getString("TIFFFaxDecoder1")); + } else { + // 11 bits - 0000 0111 1111 1111 = 0x07ff + code = (entry >>> 5) & 0x07ff; + runLength += code; + updatePointer(10 - bits); + if (isT == 0) { + isWhite = false; + } + } + } + + return runLength; + } + + // Returns run length + private int decodeBlackCodeWord() { + int current; + int entry; + int bits; + int isT; + int code = -1; + int runLength = 0; + boolean isWhite = false; + + while (!isWhite) { + current = nextLesserThan8Bits(4); + entry = initBlack[current]; + + // Get the 3 fields from the entry +// isT = entry & 0x0001; + bits = (entry >>> 1) & 0x000f; + code = (entry >>> 5) & 0x07ff; + + if (code == 100) { + current = nextNBits(9); + entry = black[current]; + + // Get the 3 fields from the entry + isT = entry & 0x0001; + bits = (entry >>> 1) & 0x000f; + code = (entry >>> 5) & 0x07ff; + + if (bits == 12) { + // Additional makeup codes + updatePointer(5); + current = nextLesserThan8Bits(4); + entry = additionalMakeup[current]; + bits = (entry >>> 1) & 0x07; // 3 bits 0000 0111 + code = (entry >>> 4) & 0x0fff; // 12 bits + runLength += code; + + updatePointer(4 - bits); + } else if (bits == 15) { + // EOL code + throw new RuntimeException(PropertyUtil.getString("TIFFFaxDecoder2")); + } else { + runLength += code; + updatePointer(9 - bits); + if (isT == 0) { + isWhite = true; + } + } + } else if (code == 200) { + // Is a Terminating code + current = nextLesserThan8Bits(2); + entry = twoBitBlack[current]; + code = (entry >>> 5) & 0x07ff; + runLength += code; + bits = (entry >>> 1) & 0x0f; + updatePointer(2 - bits); + isWhite = true; + } else { + // Is a Terminating code + runLength += code; + updatePointer(4 - bits); + isWhite = true; + } + } + + return runLength; + } + + private int readEOL() { + if (fillBits == 0) { + if (nextNBits(12) != 1) { + throw new RuntimeException(PropertyUtil.getString("TIFFFaxDecoder6")); + } + } else if (fillBits == 1) { + + // First EOL code word xxxx 0000 0000 0001 will occur + // As many fill bits will be present as required to make + // the EOL code of 12 bits end on a byte boundary. + + int bitsLeft = 8 - bitPointer; + + if (nextNBits(bitsLeft) != 0) { + throw new RuntimeException(PropertyUtil.getString("TIFFFaxDecoder8")); + } + + // If the number of bitsLeft is less than 8, then to have a 12 + // bit EOL sequence, two more bytes are certainly going to be + // required. The first of them has to be all zeros, so ensure + // that. + if (bitsLeft < 4) { + if (nextNBits(8) != 0) { + throw new RuntimeException(PropertyUtil.getString("TIFFFaxDecoder8")); + } + } + + // There might be a random number of fill bytes with 0s, so + // loop till the EOL of 0000 0001 is found, as long as all + // the bytes preceding it are 0's. + int n; + while ((n = nextNBits(8)) != 1) { + + // If not all zeros + if (n != 0) { + throw new RuntimeException(PropertyUtil.getString("TIFFFaxDecoder8")); + } + } + } + + // If one dimensional encoding mode, then always return 1 + if (oneD == 0) { + return 1; + } else { + // Otherwise for 2D encoding mode, + // The next one bit signifies 1D/2D encoding of next line. + return nextLesserThan8Bits(1); + } + } + + private void getNextChangingElement(int a0, boolean isWhite, int[] ret) { + // Local copies of instance variables + int[] pce = this.prevChangingElems; + int ces = this.changingElemSize; + + // If the previous match was at an odd element, we still + // have to search the preceeding element. + // int start = lastChangingElement & ~0x1; + int start = lastChangingElement > 0 ? lastChangingElement - 1 : 0; + if (isWhite) { + start &= ~0x1; // Search even numbered elements + } else { + start |= 0x1; // Search odd numbered elements + } + + int i = start; + for (; i < ces; i += 2) { + int temp = pce[i]; + if (temp > a0) { + lastChangingElement = i; + ret[0] = temp; + break; + } + } + + if (i + 1 < ces) { + ret[1] = pce[i + 1]; + } + } + + private int nextNBits(int bitsToGet) { + byte b; + byte next; + byte next2next; + int l = data.length - 1; + int bp = this.bytePointer; + + if (fillOrder == 1) { + b = data[bp]; + + if (bp == l) { + next = 0x00; + next2next = 0x00; + } else if ((bp + 1) == l) { + next = data[bp + 1]; + next2next = 0x00; + } else { + next = data[bp + 1]; + next2next = data[bp + 2]; + } + } else if (fillOrder == 2) { + b = flipTable[data[bp] & 0xff]; + + if (bp == l) { + next = 0x00; + next2next = 0x00; + } else if ((bp + 1) == l) { + next = flipTable[data[bp + 1] & 0xff]; + next2next = 0x00; + } else { + next = flipTable[data[bp + 1] & 0xff]; + next2next = flipTable[data[bp + 2] & 0xff]; + } + } else { + throw new RuntimeException(PropertyUtil.getString("TIFFFaxDecoder7")); + } + + int bitsLeft = 8 - bitPointer; + int bitsFromNextByte = bitsToGet - bitsLeft; + int bitsFromNext2NextByte = 0; + if (bitsFromNextByte > 8) { + bitsFromNext2NextByte = bitsFromNextByte - 8; + bitsFromNextByte = 8; + } + + bytePointer++; + + int i1 = (b & table1[bitsLeft]) << (bitsToGet - bitsLeft); + int i2 = (next & table2[bitsFromNextByte]) >>> (8 - bitsFromNextByte); + + int i3 = 0; + if (bitsFromNext2NextByte != 0) { + i2 <<= bitsFromNext2NextByte; + i3 = (next2next & table2[bitsFromNext2NextByte]) + >>> (8 - bitsFromNext2NextByte); + i2 |= i3; + bytePointer++; + bitPointer = bitsFromNext2NextByte; + } else { + if (bitsFromNextByte == 8) { + bitPointer = 0; + bytePointer++; + } else { + bitPointer = bitsFromNextByte; + } + } + + int i = i1 | i2; + return i; + } + + private int nextLesserThan8Bits(int bitsToGet) { + byte b; + byte next; + int l = data.length - 1; + int bp = this.bytePointer; + + if (fillOrder == 1) { + b = data[bp]; + if (bp == l) { + next = 0x00; + } else { + next = data[bp + 1]; + } + } else if (fillOrder == 2) { + b = flipTable[data[bp] & 0xff]; + if (bp == l) { + next = 0x00; + } else { + next = flipTable[data[bp + 1] & 0xff]; + } + } else { + throw new RuntimeException(PropertyUtil.getString("TIFFFaxDecoder7")); + } + + int bitsLeft = 8 - bitPointer; + int bitsFromNextByte = bitsToGet - bitsLeft; + + int shift = bitsLeft - bitsToGet; + int i1; + int i2; + if (shift >= 0) { + i1 = (b & table1[bitsLeft]) >>> shift; + bitPointer += bitsToGet; + if (bitPointer == 8) { + bitPointer = 0; + bytePointer++; + } + } else { + i1 = (b & table1[bitsLeft]) << (-shift); + i2 = (next & table2[bitsFromNextByte]) >>> (8 - bitsFromNextByte); + + i1 |= i2; + bytePointer++; + bitPointer = bitsFromNextByte; + } + + return i1; + } + + // Move pointer backwards by given amount of bits + private void updatePointer(int bitsToMoveBack) { + int i = bitPointer - bitsToMoveBack; + + if (i < 0) { + bytePointer--; + bitPointer = 8 + i; + } else { + bitPointer = i; + } + } + + // Move to the next byte boundary + private boolean advancePointer() { + if (bitPointer != 0) { + bytePointer++; + bitPointer = 0; + } + + return true; + } +} + diff --git a/src/main/java/org/apache/xmlgraphics/image/codec/tiff/TIFFField.java b/src/main/java/org/apache/xmlgraphics/image/codec/tiff/TIFFField.java new file mode 100644 index 0000000..8718c16 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/codec/tiff/TIFFField.java @@ -0,0 +1,462 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: TIFFField.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.image.codec.tiff; + +import java.io.Serializable; + +// CSOFF: WhitespaceAround + +/** + * A class representing a field in a TIFF 6.0 Image File Directory. + * + *

    The TIFF file format is described in more detail in the + * comments for the TIFFDescriptor class. + * + *

    A field in a TIFF Image File Directory (IFD). A field is defined + * as a sequence of values of identical data type. TIFF 6.0 defines + * 12 data types, which are mapped internally onto the Java datatypes + * byte, int, long, float, and double. + * + *

    This class is not a committed part of the JAI API. It may + * be removed or changed in future releases of JAI. + * + * @see TIFFDirectory + */ +public class TIFFField implements Comparable, Serializable { + + /** Flag for 8 bit unsigned integers. */ + public static final int TIFF_BYTE = 1; + + /** Flag for null-terminated ASCII strings. */ + public static final int TIFF_ASCII = 2; + + /** Flag for 16 bit unsigned integers. */ + public static final int TIFF_SHORT = 3; + + /** Flag for 32 bit unsigned integers. */ + public static final int TIFF_LONG = 4; + + /** Flag for pairs of 32 bit unsigned integers. */ + public static final int TIFF_RATIONAL = 5; + + /** Flag for 8 bit signed integers. */ + public static final int TIFF_SBYTE = 6; + + /** Flag for 8 bit uninterpreted bytes. */ + public static final int TIFF_UNDEFINED = 7; + + /** Flag for 16 bit signed integers. */ + public static final int TIFF_SSHORT = 8; + + /** Flag for 32 bit signed integers. */ + public static final int TIFF_SLONG = 9; + + /** Flag for pairs of 32 bit signed integers. */ + public static final int TIFF_SRATIONAL = 10; + + /** Flag for 32 bit IEEE floats. */ + public static final int TIFF_FLOAT = 11; + + /** Flag for 64 bit IEEE doubles. */ + public static final int TIFF_DOUBLE = 12; + private static final long serialVersionUID = 207783128222415437L; + + /** The tag number. */ + int tag; + + /** The tag type. */ + int type; + + /** The number of data items present in the field. */ + int count; + + /** The field data. */ + Object data; + + /** The default constructor. */ + TIFFField() { } + + /** + * Constructs a TIFFField with arbitrary data. The data + * parameter must be an array of a Java type appropriate for the + * type of the TIFF field. Since there is no available 32-bit + * unsigned datatype, long is used. The mapping between types is + * as follows: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    TIFF type Java type
    TIFF_BYTE byte
    TIFF_ASCII String
    TIFF_SHORT char
    TIFF_LONG long
    TIFF_RATIONAL long[2]
    TIFF_SBYTE byte
    TIFF_UNDEFINED byte
    TIFF_SSHORT short
    TIFF_SLONG int
    TIFF_SRATIONAL int[2]
    TIFF_FLOAT float
    TIFF_DOUBLE double
    + */ + public TIFFField(int tag, int type, int count, Object data) { + this.tag = tag; + this.type = type; + this.count = count; + this.data = data; + } + + /** + * Returns the tag number, between 0 and 65535. + */ + public int getTag() { + return tag; + } + + /** + * Returns the type of the data stored in the IFD. + * For a TIFF6.0 file, the value will equal one of the + * TIFF_ constants defined in this class. For future + * revisions of TIFF, higher values are possible. + * + */ + public int getType() { + return type; + } + + /** + * Returns the number of elements in the IFD. + */ + public int getCount() { + return count; + } + + /** + * Returns the data as an uninterpreted array of bytes. + * The type of the field must be one of TIFF_BYTE, TIFF_SBYTE, + * or TIFF_UNDEFINED; + * + *

    For data in TIFF_BYTE format, the application must take + * care when promoting the data to longer integral types + * to avoid sign extension. + * + *

    A ClassCastException will be thrown if the field is not + * of type TIFF_BYTE, TIFF_SBYTE, or TIFF_UNDEFINED. + */ + public byte[] getAsBytes() { + return (byte[])data; + } + + /** + * Returns TIFF_SHORT data as an array of chars (unsigned 16-bit + * integers). + * + *

    A ClassCastException will be thrown if the field is not + * of type TIFF_SHORT. + */ + public char[] getAsChars() { + return (char[])data; + } + + /** + * Returns TIFF_SSHORT data as an array of shorts (signed 16-bit + * integers). + * + *

    A ClassCastException will be thrown if the field is not + * of type TIFF_SSHORT. + */ + public short[] getAsShorts() { + return (short[])data; + } + + /** + * Returns TIFF_SLONG data as an array of ints (signed 32-bit + * integers). + * + *

    A ClassCastException will be thrown if the field is not + * of type TIFF_SLONG. + */ + public int[] getAsInts() { + return (int[])data; + } + + /** + * Returns TIFF_LONG data as an array of longs (signed 64-bit + * integers). + * + *

    A ClassCastException will be thrown if the field is not + * of type TIFF_LONG. + */ + public long[] getAsLongs() { + return (long[])data; + } + + /** + * Returns TIFF_FLOAT data as an array of floats. + * + *

    A ClassCastException will be thrown if the field is not + * of type TIFF_FLOAT. + */ + public float[] getAsFloats() { + return (float[])data; + } + + /** + * Returns TIFF_DOUBLE data as an array of doubles. + * + *

    A ClassCastException will be thrown if the field is not + * of type TIFF_DOUBLE. + */ + public double[] getAsDoubles() { + return (double[])data; + } + + /** + * Returns TIFF_SRATIONAL data as an array of 2-element arrays of ints. + * + *

    A ClassCastException will be thrown if the field is not + * of type TIFF_SRATIONAL. + */ + public int[][] getAsSRationals() { + return (int[][])data; + } + + /** + * Returns TIFF_RATIONAL data as an array of 2-element arrays of longs. + * + *

    A ClassCastException will be thrown if the field is not + * of type TIFF_RATTIONAL. + */ + public long[][] getAsRationals() { + return (long[][])data; + } + + /** + * Returns data in TIFF_BYTE, TIFF_SBYTE, TIFF_UNDEFINED, TIFF_SHORT, + * TIFF_SSHORT, or TIFF_SLONG format as an int. + * + *

    TIFF_BYTE and TIFF_UNDEFINED data are treated as unsigned; + * that is, no sign extension will take place and the returned + * value will be in the range [0, 255]. TIFF_SBYTE data will + * be returned in the range [-128, 127]. + * + *

    A ClassCastException will be thrown if the field is not of + * type TIFF_BYTE, TIFF_SBYTE, TIFF_UNDEFINED, TIFF_SHORT, + * TIFF_SSHORT, or TIFF_SLONG. + */ + public int getAsInt(int index) { + switch (type) { + case TIFF_BYTE: case TIFF_UNDEFINED: + return ((byte[])data)[index] & 0xff; + case TIFF_SBYTE: + return ((byte[])data)[index]; + case TIFF_SHORT: + return ((char[])data)[index] & 0xffff; + case TIFF_SSHORT: + return ((short[])data)[index]; + case TIFF_SLONG: + return ((int[])data)[index]; + default: + throw new ClassCastException(); + } + } + + /** + * Returns data in TIFF_BYTE, TIFF_SBYTE, TIFF_UNDEFINED, TIFF_SHORT, + * TIFF_SSHORT, TIFF_SLONG, or TIFF_LONG format as a long. + * + *

    TIFF_BYTE and TIFF_UNDEFINED data are treated as unsigned; + * that is, no sign extension will take place and the returned + * value will be in the range [0, 255]. TIFF_SBYTE data will + * be returned in the range [-128, 127]. + * + *

    A ClassCastException will be thrown if the field is not of + * type TIFF_BYTE, TIFF_SBYTE, TIFF_UNDEFINED, TIFF_SHORT, + * TIFF_SSHORT, TIFF_SLONG, or TIFF_LONG. + */ + public long getAsLong(int index) { + switch (type) { + case TIFF_BYTE: case TIFF_UNDEFINED: + return ((byte[])data)[index] & 0xff; + case TIFF_SBYTE: + return ((byte[])data)[index]; + case TIFF_SHORT: + return ((char[])data)[index] & 0xffff; + case TIFF_SSHORT: + return ((short[])data)[index]; + case TIFF_SLONG: + return ((int[])data)[index]; + case TIFF_LONG: + return ((long[])data)[index]; + default: + throw new ClassCastException(); + } + } + + /** + * Returns data in any numerical format as a float. Data in + * TIFF_SRATIONAL or TIFF_RATIONAL format are evaluated by + * dividing the numerator into the denominator using + * double-precision arithmetic and then truncating to single + * precision. Data in TIFF_SLONG, TIFF_LONG, or TIFF_DOUBLE + * format may suffer from truncation. + * + *

    A ClassCastException will be thrown if the field is + * of type TIFF_UNDEFINED or TIFF_ASCII. + */ + public float getAsFloat(int index) { + switch (type) { + case TIFF_BYTE: + return ((byte[])data)[index] & 0xff; + case TIFF_SBYTE: + return ((byte[])data)[index]; + case TIFF_SHORT: + return ((char[])data)[index] & 0xffff; + case TIFF_SSHORT: + return ((short[])data)[index]; + case TIFF_SLONG: + return ((int[])data)[index]; + case TIFF_LONG: + return ((long[])data)[index]; + case TIFF_FLOAT: + return ((float[])data)[index]; + case TIFF_DOUBLE: + return (float)((double[])data)[index]; + case TIFF_SRATIONAL: + int[] ivalue = getAsSRational(index); + return (float)((double)ivalue[0] / ivalue[1]); + case TIFF_RATIONAL: + long[] lvalue = getAsRational(index); + return (float)((double)lvalue[0] / lvalue[1]); + default: + throw new ClassCastException(); + } + } + + /** + * Returns data in any numerical format as a float. Data in + * TIFF_SRATIONAL or TIFF_RATIONAL format are evaluated by + * dividing the numerator into the denominator using + * double-precision arithmetic. + * + *

    A ClassCastException will be thrown if the field is of + * type TIFF_UNDEFINED or TIFF_ASCII. + */ + public double getAsDouble(int index) { + switch (type) { + case TIFF_BYTE: + return ((byte[])data)[index] & 0xff; + case TIFF_SBYTE: + return ((byte[])data)[index]; + case TIFF_SHORT: + return ((char[])data)[index] & 0xffff; + case TIFF_SSHORT: + return ((short[])data)[index]; + case TIFF_SLONG: + return ((int[])data)[index]; + case TIFF_LONG: + return ((long[])data)[index]; + case TIFF_FLOAT: + return ((float[])data)[index]; + case TIFF_DOUBLE: + return ((double[])data)[index]; + case TIFF_SRATIONAL: + int[] ivalue = getAsSRational(index); + return (double)ivalue[0] / ivalue[1]; + case TIFF_RATIONAL: + long[] lvalue = getAsRational(index); + return (double)lvalue[0] / lvalue[1]; + default: + throw new ClassCastException(); + } + } + + /** + * Returns a TIFF_ASCII data item as a String. + * + *

    A ClassCastException will be thrown if the field is not + * of type TIFF_ASCII. + */ + public String getAsString(int index) { + return ((String[])data)[index]; + } + + /** + * Returns a TIFF_SRATIONAL data item as a two-element array + * of ints. + * + *

    A ClassCastException will be thrown if the field is not + * of type TIFF_SRATIONAL. + */ + public int[] getAsSRational(int index) { + return ((int[][])data)[index]; + } + + /** + * Returns a TIFF_RATIONAL data item as a two-element array + * of ints. + * + *

    A ClassCastException will be thrown if the field is not + * of type TIFF_RATIONAL. + */ + public long[] getAsRational(int index) { + return ((long[][])data)[index]; + } + + /** + * Compares this TIFFField with another + * TIFFField by comparing the tags. + * + *

    Note: this class has a natural ordering that is inconsistent + * with equals(). + * + * @throws NullPointerException if the parameter is null. + * @throws ClassCastException if the parameter is not a + * TIFFField. + */ + public int compareTo(Object o) { + if (o == null) { + throw new NullPointerException(); + } + + int oTag = ((TIFFField)o).getTag(); + + if (tag < oTag) { + return -1; + } else if (tag > oTag) { + return 1; + } else { + return 0; + } + } +} diff --git a/src/main/java/org/apache/xmlgraphics/image/codec/tiff/TIFFImage.java b/src/main/java/org/apache/xmlgraphics/image/codec/tiff/TIFFImage.java new file mode 100644 index 0000000..c785aab --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/codec/tiff/TIFFImage.java @@ -0,0 +1,1743 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: TIFFImage.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.image.codec.tiff; + +import java.awt.Rectangle; +import java.awt.Transparency; +import java.awt.color.ColorSpace; +import java.awt.image.ColorModel; +import java.awt.image.ComponentColorModel; +import java.awt.image.DataBuffer; +import java.awt.image.DataBufferByte; +import java.awt.image.DataBufferInt; +import java.awt.image.DataBufferShort; +import java.awt.image.DataBufferUShort; +import java.awt.image.IndexColorModel; +import java.awt.image.MultiPixelPackedSampleModel; +import java.awt.image.PixelInterleavedSampleModel; +import java.awt.image.Raster; +import java.awt.image.SampleModel; +import java.awt.image.WritableRaster; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.zip.DataFormatException; +import java.util.zip.Inflater; + +import org.apache.xmlgraphics.image.codec.util.PropertyUtil; +import org.apache.xmlgraphics.image.codec.util.SeekableStream; +import org.apache.xmlgraphics.image.rendered.AbstractRed; +import org.apache.xmlgraphics.image.rendered.CachableRed; + +// CSOFF: LocalVariableName +// CSOFF: MissingSwitchDefault +// CSOFF: MultipleVariableDeclarations +// CSOFF: OperatorWrap +// CSOFF: WhitespaceAround + +public class TIFFImage extends AbstractRed { + + // Compression types + public static final int COMP_NONE = 1; + public static final int COMP_FAX_G3_1D = 2; + public static final int COMP_FAX_G3_2D = 3; + public static final int COMP_FAX_G4_2D = 4; + public static final int COMP_LZW = 5; + public static final int COMP_JPEG_OLD = 6; + public static final int COMP_JPEG_TTN2 = 7; + public static final int COMP_PACKBITS = 32773; + public static final int COMP_DEFLATE = 32946; + + // Image types + private static final int TYPE_UNSUPPORTED = -1; + private static final int TYPE_BILEVEL = 0; + private static final int TYPE_GRAY_4BIT = 1; + private static final int TYPE_GRAY = 2; + private static final int TYPE_GRAY_ALPHA = 3; + private static final int TYPE_PALETTE = 4; + private static final int TYPE_RGB = 5; + private static final int TYPE_RGB_ALPHA = 6; + private static final int TYPE_YCBCR_SUB = 7; + private static final int TYPE_GENERIC = 8; + + // Incidental tags + private static final int TIFF_JPEG_TABLES = 347; + private static final int TIFF_YCBCR_SUBSAMPLING = 530; + + SeekableStream stream; + int tileSize; + int tilesX; + int tilesY; + long[] tileOffsets; + long[] tileByteCounts; + char[] colormap; + int sampleSize; + int compression; + byte[] palette; + int numBands; + + int chromaSubH; + int chromaSubV; + + // Fax compression related variables + long tiffT4Options; + long tiffT6Options; + int fillOrder; + + // LZW compression related variable + int predictor; + + // DEFLATE variables + Inflater inflater; + + // Endian-ness indicator + boolean isBigEndian; + + int imageType; + boolean isWhiteZero; + int dataType; + + boolean decodePaletteAsShorts; + boolean tiled; + + // Decoders + private TIFFFaxDecoder decoder; + private TIFFLZWDecoder lzwDecoder; + + /** + * Inflates deflated into inflated using the + * Inflater constructed during class instantiation. + */ + private void inflate(byte[] deflated, byte[] inflated) { + inflater.setInput(deflated); + try { + inflater.inflate(inflated); + } catch (DataFormatException dfe) { + throw new RuntimeException(PropertyUtil.getString("TIFFImage17") + ": " + + dfe.getMessage()); + } + inflater.reset(); + } + + private static SampleModel createPixelInterleavedSampleModel( + int dataType, int tileWidth, int tileHeight, int bands) { + int [] bandOffsets = new int[bands]; + for (int i = 0; i < bands; i++) { + bandOffsets[i] = i; + } + return new PixelInterleavedSampleModel( + dataType, tileWidth, tileHeight, bands, + tileWidth * bands, bandOffsets); + } + + /** + * Return as a long[] the value of a TIFF_LONG or TIFF_SHORT field. + */ + private long[] getFieldAsLongs(TIFFField field) { + long[] value = null; + + if (field.getType() == TIFFField.TIFF_SHORT) { + char[] charValue = field.getAsChars(); + value = new long[charValue.length]; + for (int i = 0; i < charValue.length; i++) { + value[i] = charValue[i] & 0xffff; + } + } else if (field.getType() == TIFFField.TIFF_LONG) { + value = field.getAsLongs(); + } else { + throw new RuntimeException(PropertyUtil.getString("TIFFImage18") + ": " + + field.getType()); + } + + return value; + } + + /** + * Constructs a TIFFImage that acquires its data from a given + * SeekableStream and reads from a particular IFD of the stream. + * The index of the first IFD is 0. + * + * @param stream the SeekableStream to read from. + * @param param an instance of TIFFDecodeParam, or null. + * @param directory the index of the IFD to read from. + */ + public TIFFImage(SeekableStream stream, + TIFFDecodeParam param, + int directory) + throws IOException { + + this.stream = stream; + if (param == null) { + param = new TIFFDecodeParam(); + } + + decodePaletteAsShorts = param.getDecodePaletteAsShorts(); + + // Read the specified directory. + TIFFDirectory dir = param.getIFDOffset() == null + ? new TIFFDirectory(stream, directory) + : new TIFFDirectory(stream, param.getIFDOffset(), + directory); + + // Get the number of samples per pixel + TIFFField sfield = dir.getField(TIFFImageDecoder.TIFF_SAMPLES_PER_PIXEL); + int samplesPerPixel = sfield == null ? 1 : (int)sfield.getAsLong(0); + + // Read the TIFF_PLANAR_CONFIGURATION field + TIFFField planarConfigurationField = + dir.getField(TIFFImageDecoder.TIFF_PLANAR_CONFIGURATION); + char[] planarConfiguration = planarConfigurationField == null + ? new char[] {1} + : planarConfigurationField.getAsChars(); + + // Support planar format (band sequential) only for 1 sample/pixel. + if (planarConfiguration[0] != 1 && samplesPerPixel != 1) { + throw new RuntimeException(PropertyUtil.getString("TIFFImage0")); + } + + // Read the TIFF_BITS_PER_SAMPLE field + TIFFField bitsField = + dir.getField(TIFFImageDecoder.TIFF_BITS_PER_SAMPLE); + char[] bitsPerSample = null; + if (bitsField != null) { + bitsPerSample = bitsField.getAsChars(); + } else { + bitsPerSample = new char[] {1}; + + // Ensure that all samples have the same bit depth. + for (int i = 1; i < bitsPerSample.length; i++) { + if (bitsPerSample[i] != bitsPerSample[0]) { + throw new RuntimeException(PropertyUtil.getString("TIFFImage1")); + } + } + } + sampleSize = bitsPerSample[0]; + + // Read the TIFF_SAMPLE_FORMAT tag to see whether the data might be + // signed or floating point + TIFFField sampleFormatField = + dir.getField(TIFFImageDecoder.TIFF_SAMPLE_FORMAT); + + char[] sampleFormat = null; + if (sampleFormatField != null) { + sampleFormat = sampleFormatField.getAsChars(); + + // Check that all the samples have the same format + for (int l = 1; l < sampleFormat.length; l++) { + if (sampleFormat[l] != sampleFormat[0]) { + throw new RuntimeException(PropertyUtil.getString("TIFFImage2")); + } + } + + } else { + sampleFormat = new char[] {1}; + } + + // Set the data type based on the sample size and format. + boolean isValidDataFormat = false; + switch (sampleSize) { + case 1: + case 4: + case 8: + if (sampleFormat[0] != 3) { + // Ignore whether signed or unsigned: treat all as unsigned. + dataType = DataBuffer.TYPE_BYTE; + isValidDataFormat = true; + } + break; + case 16: + if (sampleFormat[0] != 3) { + dataType = sampleFormat[0] == 2 + ? DataBuffer.TYPE_SHORT : DataBuffer.TYPE_USHORT; + isValidDataFormat = true; + } + break; + case 32: + if (sampleFormat[0] == 3) { + isValidDataFormat = false; + } else { + dataType = DataBuffer.TYPE_INT; + isValidDataFormat = true; + } + break; + } + + if (!isValidDataFormat) { + throw new RuntimeException(PropertyUtil.getString("TIFFImage3")); + } + + // Figure out what compression if any, is being used. + TIFFField compField = dir.getField(TIFFImageDecoder.TIFF_COMPRESSION); + compression = compField == null ? COMP_NONE : compField.getAsInt(0); + + // Get the photometric interpretation. + int photometricType; + TIFFField photometricTypeField = dir.getField( + TIFFImageDecoder.TIFF_PHOTOMETRIC_INTERPRETATION); + if (photometricTypeField == null) { + photometricType = 0; // White is zero + } else { + photometricType = photometricTypeField.getAsInt(0); + } + + // Determine which kind of image we are dealing with. + imageType = TYPE_UNSUPPORTED; + switch(photometricType) { + case 0: // WhiteIsZero + isWhiteZero = true; + case 1: // BlackIsZero + if (sampleSize == 1 && samplesPerPixel == 1) { + imageType = TYPE_BILEVEL; + } else if (sampleSize == 4 && samplesPerPixel == 1) { + imageType = TYPE_GRAY_4BIT; + } else if (sampleSize % 8 == 0) { + if (samplesPerPixel == 1) { + imageType = TYPE_GRAY; + } else if (samplesPerPixel == 2) { + imageType = TYPE_GRAY_ALPHA; + } else { + imageType = TYPE_GENERIC; + } + } + break; + case 2: // RGB + if (sampleSize % 8 == 0) { + if (samplesPerPixel == 3) { + imageType = TYPE_RGB; + } else if (samplesPerPixel == 4) { + imageType = TYPE_RGB_ALPHA; + } else { + imageType = TYPE_GENERIC; + } + } + break; + case 3: // RGB Palette + if (samplesPerPixel == 1 + && (sampleSize == 4 || sampleSize == 8 || sampleSize == 16)) { + imageType = TYPE_PALETTE; + } + break; + case 4: // Transparency mask + if (sampleSize == 1 && samplesPerPixel == 1) { + imageType = TYPE_BILEVEL; + } + break; + default: // Other including CMYK, CIE L*a*b*, unknown. + if (sampleSize % 8 == 0) { + imageType = TYPE_GENERIC; + } + } + + // Bail out if not one of the supported types. + if (imageType == TYPE_UNSUPPORTED) { + throw new RuntimeException(PropertyUtil.getString("TIFFImage4") + ": " + + imageType); + } + + // Set basic image layout + Rectangle bounds = new Rectangle( + 0, 0, + (int)dir.getFieldAsLong(TIFFImageDecoder.TIFF_IMAGE_WIDTH), + (int)dir.getFieldAsLong(TIFFImageDecoder.TIFF_IMAGE_LENGTH)); + + // Set a preliminary band count. This may be changed later as needed. + numBands = samplesPerPixel; + + // Figure out if any extra samples are present. + TIFFField efield = dir.getField(TIFFImageDecoder.TIFF_EXTRA_SAMPLES); + int extraSamples = efield == null ? 0 : (int)efield.getAsLong(0); + + int tileWidth; + int tileHeight; + if (dir.getField(TIFFImageDecoder.TIFF_TILE_OFFSETS) != null) { + tiled = true; + // Image is in tiled format + tileWidth = + (int)dir.getFieldAsLong(TIFFImageDecoder.TIFF_TILE_WIDTH); + tileHeight = + (int)dir.getFieldAsLong(TIFFImageDecoder.TIFF_TILE_LENGTH); + tileOffsets = + (dir.getField(TIFFImageDecoder.TIFF_TILE_OFFSETS)).getAsLongs(); + tileByteCounts = + getFieldAsLongs(dir.getField(TIFFImageDecoder.TIFF_TILE_BYTE_COUNTS)); + + } else { + tiled = false; + + // Image is in stripped format, looks like tiles to us + // Note: Some legacy files may have tile width and height + // written but use the strip offsets and byte counts fields + // instead of the tile offsets and byte counts. Therefore + // we default here to the tile dimensions if they are written. + tileWidth = + dir.getField(TIFFImageDecoder.TIFF_TILE_WIDTH) != null + ? (int)dir.getFieldAsLong(TIFFImageDecoder.TIFF_TILE_WIDTH) + : bounds.width; + TIFFField field = + dir.getField(TIFFImageDecoder.TIFF_ROWS_PER_STRIP); + if (field == null) { + // Default is infinity (2^32 -1), basically the entire image + + tileHeight = + dir.getField(TIFFImageDecoder.TIFF_TILE_LENGTH) != null + ? (int)dir.getFieldAsLong(TIFFImageDecoder.TIFF_TILE_LENGTH) + : bounds.height; + } else { + long l = field.getAsLong(0); + long infinity = 1; + infinity = (infinity << 32) - 1; + if (l == infinity) { + // 2^32 - 1 (effectively infinity, entire image is 1 strip) + tileHeight = bounds.height; + } else { + tileHeight = (int)l; + } + } + + TIFFField tileOffsetsField = + dir.getField(TIFFImageDecoder.TIFF_STRIP_OFFSETS); + if (tileOffsetsField == null) { + throw new RuntimeException(PropertyUtil.getString("TIFFImage5")); + } else { + tileOffsets = getFieldAsLongs(tileOffsetsField); + } + + TIFFField tileByteCountsField = + dir.getField(TIFFImageDecoder.TIFF_STRIP_BYTE_COUNTS); + if (tileByteCountsField == null) { + throw new RuntimeException(PropertyUtil.getString("TIFFImage6")); + } else { + tileByteCounts = getFieldAsLongs(tileByteCountsField); + } + } + + // Calculate number of tiles and the tileSize in bytes + tilesX = (bounds.width + tileWidth - 1) / tileWidth; + tilesY = (bounds.height + tileHeight - 1) / tileHeight; + tileSize = tileWidth * tileHeight * numBands; + + // Check whether big endian or little endian format is used. + isBigEndian = dir.isBigEndian(); + + TIFFField fillOrderField = + dir.getField(TIFFImageDecoder.TIFF_FILL_ORDER); + if (fillOrderField != null) { + fillOrder = fillOrderField.getAsInt(0); + } else { + // Default Fill Order + fillOrder = 1; + } + + switch(compression) { + case COMP_NONE: + case COMP_PACKBITS: + // Do nothing. + break; + case COMP_DEFLATE: + inflater = new Inflater(); + break; + case COMP_FAX_G3_1D: + case COMP_FAX_G3_2D: + case COMP_FAX_G4_2D: + if (sampleSize != 1) { + throw new RuntimeException(PropertyUtil.getString("TIFFImage7")); + } + + // Fax T.4 compression options + if (compression == 3) { + TIFFField t4OptionsField = + dir.getField(TIFFImageDecoder.TIFF_T4_OPTIONS); + if (t4OptionsField != null) { + tiffT4Options = t4OptionsField.getAsLong(0); + } else { + // Use default value + tiffT4Options = 0; + } + } + + // Fax T.6 compression options + if (compression == 4) { + TIFFField t6OptionsField = + dir.getField(TIFFImageDecoder.TIFF_T6_OPTIONS); + if (t6OptionsField != null) { + tiffT6Options = t6OptionsField.getAsLong(0); + } else { + // Use default value + tiffT6Options = 0; + } + } + + // Fax encoding, need to create the Fax decoder. + decoder = new TIFFFaxDecoder(fillOrder, + tileWidth, tileHeight); + break; + + case COMP_LZW: + // LZW compression used, need to create the LZW decoder. + TIFFField predictorField = + dir.getField(TIFFImageDecoder.TIFF_PREDICTOR); + + if (predictorField == null) { + predictor = 1; + } else { + predictor = predictorField.getAsInt(0); + + if (predictor != 1 && predictor != 2) { + throw new RuntimeException(PropertyUtil.getString("TIFFImage8")); + } + + if (predictor == 2 && sampleSize != 8) { + throw new RuntimeException(PropertyUtil.getString("TIFFImage9")); + } + } + + lzwDecoder = new TIFFLZWDecoder(tileWidth, predictor, + samplesPerPixel); + break; + + case COMP_JPEG_OLD: + throw new RuntimeException(PropertyUtil.getString("TIFFImage15")); + + default: + throw new RuntimeException(PropertyUtil.getString("TIFFImage10") + ": " + + compression); + } + + ColorModel colorModel = null; + SampleModel sampleModel = null; + switch(imageType) { + case TYPE_BILEVEL: + case TYPE_GRAY_4BIT: + sampleModel = + new MultiPixelPackedSampleModel(dataType, + tileWidth, + tileHeight, + sampleSize); + if (imageType == TYPE_BILEVEL) { + byte[] map = new byte[] {(byte)(isWhiteZero ? 255 : 0), + (byte)(isWhiteZero ? 0 : 255)}; + colorModel = new IndexColorModel(1, 2, map, map, map); + } else { + byte [] map = new byte[16]; + if (isWhiteZero) { + for (int i = 0; i < map.length; i++) { + map[i] = (byte)(255 - (16 * i)); + } + } else { + for (int i = 0; i < map.length; i++) { + map[i] = (byte)(16 * i); + } + } + colorModel = new IndexColorModel(4, 16, map, map, map); + } + break; + + case TYPE_GRAY: + case TYPE_GRAY_ALPHA: + case TYPE_RGB: + case TYPE_RGB_ALPHA: + // Create a pixel interleaved SampleModel with decreasing + // band offsets. + int[] reverseOffsets = new int[numBands]; + for (int i = 0; i < numBands; i++) { + reverseOffsets[i] = numBands - 1 - i; + } + sampleModel = new PixelInterleavedSampleModel( + dataType, tileWidth, tileHeight, + numBands, numBands * tileWidth, reverseOffsets); + + if (imageType == TYPE_GRAY) { + colorModel = new ComponentColorModel( + ColorSpace.getInstance(ColorSpace.CS_GRAY), + new int[] {sampleSize}, false, false, + Transparency.OPAQUE, dataType); + } else if (imageType == TYPE_RGB) { + colorModel = new ComponentColorModel( + ColorSpace.getInstance(ColorSpace.CS_sRGB), + new int[] {sampleSize, sampleSize, sampleSize}, + false, false, Transparency.OPAQUE, dataType); + } else { // hasAlpha + // Transparency.OPAQUE signifies image data that is + // completely opaque, meaning that all pixels have an alpha + // value of 1.0. So the extra band gets ignored, which is + // what we want. + int transparency = Transparency.OPAQUE; + if (extraSamples == 1) { // associated (premultiplied) alpha + transparency = Transparency.TRANSLUCENT; + } else if (extraSamples == 2) { // unassociated alpha + transparency = Transparency.BITMASK; + } + + colorModel = + createAlphaComponentColorModel(dataType, + numBands, + extraSamples == 1, + transparency); + } + break; + + case TYPE_GENERIC: + case TYPE_YCBCR_SUB: + // For this case we can't display the image, so we create a + // SampleModel with increasing bandOffsets, and keep the + // ColorModel as null, as there is no appropriate ColorModel. + + int[] bandOffsets = new int[numBands]; + for (int i = 0; i < numBands; i++) { + bandOffsets[i] = i; + } + + sampleModel = new PixelInterleavedSampleModel( + dataType, tileWidth, tileHeight, + numBands, numBands * tileWidth, bandOffsets); + colorModel = null; + break; + + case TYPE_PALETTE: + // Get the colormap + TIFFField cfield = dir.getField(TIFFImageDecoder.TIFF_COLORMAP); + if (cfield == null) { + throw new RuntimeException(PropertyUtil.getString("TIFFImage11")); + } else { + colormap = cfield.getAsChars(); + } + + // Could be either 1 or 3 bands depending on whether we use + // IndexColorModel or not. + if (decodePaletteAsShorts) { + numBands = 3; + + // If no SampleFormat tag was specified and if the + // sampleSize is less than or equal to 8, then the + // dataType was initially set to byte, but now we want to + // expand the palette as shorts, so the dataType should + // be ushort. + if (dataType == DataBuffer.TYPE_BYTE) { + dataType = DataBuffer.TYPE_USHORT; + } + + // Data will have to be unpacked into a 3 band short image + // as we do not have a IndexColorModel that can deal with + // a colormodel whose entries are of short data type. + sampleModel = createPixelInterleavedSampleModel( + dataType, tileWidth, tileHeight, numBands); + + colorModel = new ComponentColorModel( + ColorSpace.getInstance(ColorSpace.CS_sRGB), + new int[] {16, 16, 16}, false, false, + Transparency.OPAQUE, dataType); + + } else { + + numBands = 1; + + if (sampleSize == 4) { + // Pixel data will not be unpacked, will use + // MPPSM to store packed data and + // IndexColorModel to do the unpacking. + sampleModel = new MultiPixelPackedSampleModel( + DataBuffer.TYPE_BYTE, tileWidth, tileHeight, + sampleSize); + } else if (sampleSize == 8) { + + sampleModel = createPixelInterleavedSampleModel( + DataBuffer.TYPE_BYTE, tileWidth, tileHeight, + numBands); + } else if (sampleSize == 16) { + + // Here datatype has to be unsigned since we + // are storing indices into the + // IndexColorModel palette. Ofcourse the + // actual palette entries are allowed to be + // negative. + dataType = DataBuffer.TYPE_USHORT; + sampleModel = createPixelInterleavedSampleModel( + DataBuffer.TYPE_USHORT, tileWidth, tileHeight, + numBands); + } + + int bandLength = colormap.length / 3; + byte[] r = new byte[bandLength]; + byte[] g = new byte[bandLength]; + byte[] b = new byte[bandLength]; + + int gIndex = bandLength; + int bIndex = bandLength * 2; + + if (dataType == DataBuffer.TYPE_SHORT) { + + for (int i = 0; i < bandLength; i++) { + r[i] = param.decodeSigned16BitsTo8Bits( + (short)colormap[i]); + g[i] = param.decodeSigned16BitsTo8Bits( + (short)colormap[gIndex + i]); + b[i] = param.decodeSigned16BitsTo8Bits( + (short)colormap[bIndex + i]); + } + + } else { + + for (int i = 0; i < bandLength; i++) { + r[i] = param.decode16BitsTo8Bits( + colormap[i] & 0xffff); + g[i] = param.decode16BitsTo8Bits( + colormap[gIndex + i] & 0xffff); + b[i] = param.decode16BitsTo8Bits( + colormap[bIndex + i] & 0xffff); + } + + } + + colorModel = new IndexColorModel(sampleSize, + bandLength, r, g, b); + } + break; + + default: + throw new RuntimeException(PropertyUtil.getString("TIFFImage4") + ": " + + imageType); + } + + Map properties = new HashMap(); + // Set a property "tiff_directory". + properties.put("tiff_directory", dir); + + // System.out.println("Constructed TIFF"); + + init((CachableRed)null, bounds, colorModel, sampleModel, + 0, 0, properties); + } + + /** + * Reads a private IFD from a given offset in the stream. This + * method may be used to obtain IFDs that are referenced + * only by private tag values. + */ + public TIFFDirectory getPrivateIFD(long offset) throws IOException { + return new TIFFDirectory(stream, offset, 0); + } + + + public WritableRaster copyData(WritableRaster wr) { + copyToRaster(wr); + return wr; + } + + + /** + * Returns tile (tileX, tileY) as a Raster. + */ + public synchronized Raster getTile(int tileX, int tileY) { + if ((tileX < 0) || (tileX >= tilesX) + || (tileY < 0) || (tileY >= tilesY)) { + throw new IllegalArgumentException(PropertyUtil.getString("TIFFImage12")); + } + + // System.out.println("Called TIFF getTile:" + tileX + "," + tileY); + + + // Get the data array out of the DataBuffer + byte[] bdata = null; + short[] sdata = null; + int[] idata = null; + + SampleModel sampleModel = getSampleModel(); + WritableRaster tile = makeTile(tileX, tileY); + + DataBuffer buffer = tile.getDataBuffer(); + + int dataType = sampleModel.getDataType(); + if (dataType == DataBuffer.TYPE_BYTE) { + bdata = ((DataBufferByte)buffer).getData(); + } else if (dataType == DataBuffer.TYPE_USHORT) { + sdata = ((DataBufferUShort)buffer).getData(); + } else if (dataType == DataBuffer.TYPE_SHORT) { + sdata = ((DataBufferShort)buffer).getData(); + } else if (dataType == DataBuffer.TYPE_INT) { + idata = ((DataBufferInt)buffer).getData(); + } + + // Variables used for swapping when converting from RGB to BGR + byte bswap; + short sswap; + int iswap; + + // Save original file pointer position and seek to tile data location. + long saveOffset = 0; + try { + saveOffset = stream.getFilePointer(); + stream.seek(tileOffsets[tileY * tilesX + tileX]); + } catch (IOException ioe) { + throw new RuntimeException(PropertyUtil.getString("TIFFImage13") + ": " + + ioe.getMessage()); + } + + // Number of bytes in this tile (strip) after compression. + int byteCount = (int)tileByteCounts[tileY * tilesX + tileX]; + + // Find out the number of bytes in the current tile + Rectangle newRect; + if (!tiled) { + newRect = tile.getBounds(); + } else { + newRect = new Rectangle(tile.getMinX(), tile.getMinY(), + tileWidth, tileHeight); + } + + int unitsInThisTile = newRect.width * newRect.height * numBands; + + // Allocate read buffer if needed. + byte[] data = compression != COMP_NONE || imageType == TYPE_PALETTE + ? new byte[byteCount] : null; + + // Read the data, uncompressing as needed. There are four cases: + // bilevel, palette-RGB, 4-bit grayscale, and everything else. + if (imageType == TYPE_BILEVEL) { // bilevel + try { + if (compression == COMP_PACKBITS) { + stream.readFully(data, 0, byteCount); + + // Since the decompressed data will still be packed + // 8 pixels into 1 byte, calculate bytesInThisTile + int bytesInThisTile; + if ((newRect.width % 8) == 0) { + bytesInThisTile = (newRect.width / 8) * newRect.height; + } else { + bytesInThisTile = + (newRect.width / 8 + 1) * newRect.height; + } + decodePackbits(data, bytesInThisTile, bdata); + } else if (compression == COMP_LZW) { + stream.readFully(data, 0, byteCount); + lzwDecoder.decode(data, bdata, newRect.height); + } else if (compression == COMP_FAX_G3_1D) { + stream.readFully(data, 0, byteCount); + decoder.decode1D(bdata, data, 0, newRect.height); + } else if (compression == COMP_FAX_G3_2D) { + stream.readFully(data, 0, byteCount); + decoder.decode2D(bdata, data, 0, newRect.height, + tiffT4Options); + } else if (compression == COMP_FAX_G4_2D) { + stream.readFully(data, 0, byteCount); + decoder.decodeT6(bdata, data, 0, newRect.height, + tiffT6Options); + } else if (compression == COMP_DEFLATE) { + stream.readFully(data, 0, byteCount); + inflate(data, bdata); + } else if (compression == COMP_NONE) { + stream.readFully(bdata, 0, byteCount); + } + + stream.seek(saveOffset); + } catch (IOException ioe) { + throw new RuntimeException(PropertyUtil.getString("TIFFImage13") + ": " + + ioe.getMessage()); + } + } else if (imageType == TYPE_PALETTE) { // palette-RGB + if (sampleSize == 16) { + + if (decodePaletteAsShorts) { + + short[] tempData = null; + + // At this point the data is 1 banded and will + // become 3 banded only after we've done the palette + // lookup, since unitsInThisTile was calculated with + // 3 bands, we need to divide this by 3. + int unitsBeforeLookup = unitsInThisTile / 3; + + // Since unitsBeforeLookup is the number of shorts, + // but we do our decompression in terms of bytes, we + // need to multiply it by 2 in order to figure out + // how many bytes we'll get after decompression. + int entries = unitsBeforeLookup * 2; + + // Read the data, if compressed, decode it, reset the pointer + try { + + if (compression == COMP_PACKBITS) { + + stream.readFully(data, 0, byteCount); + + byte[] byteArray = new byte[entries]; + decodePackbits(data, entries, byteArray); + tempData = new short[unitsBeforeLookup]; + interpretBytesAsShorts(byteArray, tempData, + unitsBeforeLookup); + + } else if (compression == COMP_LZW) { + + // Read in all the compressed data for this tile + stream.readFully(data, 0, byteCount); + + byte[] byteArray = new byte[entries]; + lzwDecoder.decode(data, byteArray, newRect.height); + tempData = new short[unitsBeforeLookup]; + interpretBytesAsShorts(byteArray, tempData, + unitsBeforeLookup); + + } else if (compression == COMP_DEFLATE) { + + stream.readFully(data, 0, byteCount); + byte[] byteArray = new byte[entries]; + inflate(data, byteArray); + tempData = new short[unitsBeforeLookup]; + interpretBytesAsShorts(byteArray, tempData, + unitsBeforeLookup); + + } else if (compression == COMP_NONE) { + + // byteCount tells us how many bytes are there + // in this tile, but we need to read in shorts, + // which will take half the space, so while + // allocating we divide byteCount by 2. + tempData = new short[byteCount / 2]; + readShorts(byteCount / 2, tempData); + } + + stream.seek(saveOffset); + + } catch (IOException ioe) { + throw new RuntimeException(PropertyUtil.getString("TIFFImage13") + ": " + + ioe.getMessage()); + } + + if (dataType == DataBuffer.TYPE_USHORT) { + + // Expand the palette image into an rgb image with ushort + // data type. + int cmapValue; + int count = 0; + int lookup; + int len = colormap.length / 3; + int len2 = len * 2; + for (int i = 0; i < unitsBeforeLookup; i++) { + // Get the index into the colormap + lookup = tempData[i] & 0xffff; + // Get the blue value + cmapValue = colormap[lookup + len2]; + sdata[count++] = (short)(cmapValue & 0xffff); + // Get the green value + cmapValue = colormap[lookup + len]; + sdata[count++] = (short)(cmapValue & 0xffff); + // Get the red value + cmapValue = colormap[lookup]; + sdata[count++] = (short)(cmapValue & 0xffff); + } + + } else if (dataType == DataBuffer.TYPE_SHORT) { + + // Expand the palette image into an rgb image with + // short data type. + int cmapValue; + int count = 0; + int lookup; + int len = colormap.length / 3; + int len2 = len * 2; + for (int i = 0; i < unitsBeforeLookup; i++) { + // Get the index into the colormap + lookup = tempData[i] & 0xffff; + // Get the blue value + cmapValue = colormap[lookup + len2]; + sdata[count++] = (short)cmapValue; + // Get the green value + cmapValue = colormap[lookup + len]; + sdata[count++] = (short)cmapValue; + // Get the red value + cmapValue = colormap[lookup]; + sdata[count++] = (short)cmapValue; + } + } + + } else { + + // No lookup being done here, when RGB values are needed, + // the associated IndexColorModel can be used to get them. + + try { + + if (compression == COMP_PACKBITS) { + + stream.readFully(data, 0, byteCount); + + // Since unitsInThisTile is the number of shorts, + // but we do our decompression in terms of bytes, we + // need to multiply unitsInThisTile by 2 in order to + // figure out how many bytes we'll get after + // decompression. + int bytesInThisTile = unitsInThisTile * 2; + + byte[] byteArray = new byte[bytesInThisTile]; + decodePackbits(data, bytesInThisTile, byteArray); + interpretBytesAsShorts(byteArray, sdata, + unitsInThisTile); + + } else if (compression == COMP_LZW) { + + stream.readFully(data, 0, byteCount); + + // Since unitsInThisTile is the number of shorts, + // but we do our decompression in terms of bytes, we + // need to multiply unitsInThisTile by 2 in order to + // figure out how many bytes we'll get after + // decompression. + byte[] byteArray = new byte[unitsInThisTile * 2]; + lzwDecoder.decode(data, byteArray, newRect.height); + interpretBytesAsShorts(byteArray, sdata, + unitsInThisTile); + + } else if (compression == COMP_DEFLATE) { + + stream.readFully(data, 0, byteCount); + byte[] byteArray = new byte[unitsInThisTile * 2]; + inflate(data, byteArray); + interpretBytesAsShorts(byteArray, sdata, + unitsInThisTile); + + } else if (compression == COMP_NONE) { + + readShorts(byteCount / 2, sdata); + } + + stream.seek(saveOffset); + + } catch (IOException ioe) { + throw new RuntimeException(PropertyUtil.getString("TIFFImage13") + ": " + + ioe.getMessage()); + } + } + + } else if (sampleSize == 8) { + + if (decodePaletteAsShorts) { + + byte[] tempData = null; + + // At this point the data is 1 banded and will + // become 3 banded only after we've done the palette + // lookup, since unitsInThisTile was calculated with + // 3 bands, we need to divide this by 3. + int unitsBeforeLookup = unitsInThisTile / 3; + + // Read the data, if compressed, decode it, reset the pointer + try { + + if (compression == COMP_PACKBITS) { + + stream.readFully(data, 0, byteCount); + tempData = new byte[unitsBeforeLookup]; + decodePackbits(data, unitsBeforeLookup, tempData); + + } else if (compression == COMP_LZW) { + + stream.readFully(data, 0, byteCount); + tempData = new byte[unitsBeforeLookup]; + lzwDecoder.decode(data, tempData, newRect.height); + + } else if (compression == COMP_DEFLATE) { + + stream.readFully(data, 0, byteCount); + tempData = new byte[unitsBeforeLookup]; + inflate(data, tempData); + + } else if (compression == COMP_NONE) { + + tempData = new byte[byteCount]; + stream.readFully(tempData, 0, byteCount); + } else { + throw new RuntimeException(PropertyUtil.getString("IFFImage10") + ": " + + compression); + } + + stream.seek(saveOffset); + + } catch (IOException ioe) { + throw new RuntimeException(PropertyUtil.getString("TIFFImage13") + ": " + + ioe.getMessage()); + } + + // Expand the palette image into an rgb image with ushort + // data type. + int cmapValue; + int count = 0; + int lookup; + int len = colormap.length / 3; + int len2 = len * 2; + for (int i = 0; i < unitsBeforeLookup; i++) { + // Get the index into the colormap + lookup = tempData[i] & 0xff; + // Get the blue value + cmapValue = colormap[lookup + len2]; + sdata[count++] = (short)(cmapValue & 0xffff); + // Get the green value + cmapValue = colormap[lookup + len]; + sdata[count++] = (short)(cmapValue & 0xffff); + // Get the red value + cmapValue = colormap[lookup]; + sdata[count++] = (short)(cmapValue & 0xffff); + } + } else { + + // No lookup being done here, when RGB values are needed, + // the associated IndexColorModel can be used to get them. + + try { + + if (compression == COMP_PACKBITS) { + + stream.readFully(data, 0, byteCount); + decodePackbits(data, unitsInThisTile, bdata); + + } else if (compression == COMP_LZW) { + + stream.readFully(data, 0, byteCount); + lzwDecoder.decode(data, bdata, newRect.height); + + } else if (compression == COMP_DEFLATE) { + + stream.readFully(data, 0, byteCount); + inflate(data, bdata); + + } else if (compression == COMP_NONE) { + + stream.readFully(bdata, 0, byteCount); + + } else { + throw new RuntimeException(PropertyUtil.getString("TIFFImage10") + + ": " + compression); + } + + stream.seek(saveOffset); + + } catch (IOException ioe) { + throw new RuntimeException(PropertyUtil.getString("TIFFImage13") + ": " + + ioe.getMessage()); + } + } + + } else if (sampleSize == 4) { + + int padding = (newRect.width % 2 == 0) ? 0 : 1; + int bytesPostDecoding = ((newRect.width / 2 + padding) * newRect.height); + + // Output short images + if (decodePaletteAsShorts) { + + byte[] tempData = null; + + try { + stream.readFully(data, 0, byteCount); + stream.seek(saveOffset); + } catch (IOException ioe) { + throw new RuntimeException(PropertyUtil.getString("TIFFImage13") + ": " + + ioe.getMessage()); + } + + // If compressed, decode the data. + if (compression == COMP_PACKBITS) { + + tempData = new byte[bytesPostDecoding]; + decodePackbits(data, bytesPostDecoding, tempData); + + } else if (compression == COMP_LZW) { + + tempData = new byte[bytesPostDecoding]; + lzwDecoder.decode(data, tempData, newRect.height); + + } else if (compression == COMP_DEFLATE) { + + tempData = new byte[bytesPostDecoding]; + inflate(data, tempData); + + } else if (compression == COMP_NONE) { + + tempData = data; + } + + int bytes = unitsInThisTile / 3; + + // Unpack the 2 pixels packed into each byte. + data = new byte[bytes]; + + int srcCount = 0; + int dstCount = 0; + for (int j = 0; j < newRect.height; j++) { + for (int i = 0; i < newRect.width / 2; i++) { + data[dstCount++] = + (byte)((tempData[srcCount] & 0xf0) >> 4); + data[dstCount++] = + (byte)(tempData[srcCount++] & 0x0f); + } + + if (padding == 1) { + data[dstCount++] = + (byte)((tempData[srcCount++] & 0xf0) >> 4); + } + } + + int len = colormap.length / 3; + int len2 = len * 2; + int cmapValue; + int lookup; + int count = 0; + for (int i = 0; i < bytes; i++) { + lookup = data[i] & 0xff; + cmapValue = colormap[lookup + len2]; + sdata[count++] = (short)(cmapValue & 0xffff); + cmapValue = colormap[lookup + len]; + sdata[count++] = (short)(cmapValue & 0xffff); + cmapValue = colormap[lookup]; + sdata[count++] = (short)(cmapValue & 0xffff); + } + } else { + + // Output byte values, use IndexColorModel for unpacking + try { + + // If compressed, decode the data. + if (compression == COMP_PACKBITS) { + + stream.readFully(data, 0, byteCount); + decodePackbits(data, bytesPostDecoding, bdata); + + } else if (compression == COMP_LZW) { + + stream.readFully(data, 0, byteCount); + lzwDecoder.decode(data, bdata, newRect.height); + + } else if (compression == COMP_DEFLATE) { + + stream.readFully(data, 0, byteCount); + inflate(data, bdata); + + } else if (compression == COMP_NONE) { + + stream.readFully(bdata, 0, byteCount); + } + + stream.seek(saveOffset); + + } catch (IOException ioe) { + throw new RuntimeException(PropertyUtil.getString("TIFFImage13") + ": " + + ioe.getMessage()); + } + } + } + } else if (imageType == TYPE_GRAY_4BIT) { // 4-bit gray + try { + if (compression == COMP_PACKBITS) { + + stream.readFully(data, 0, byteCount); + + // Since the decompressed data will still be packed + // 2 pixels into 1 byte, calculate bytesInThisTile + int bytesInThisTile; + if ((newRect.width % 8) == 0) { + bytesInThisTile = (newRect.width / 2) * newRect.height; + } else { + bytesInThisTile = (newRect.width / 2 + 1) + * newRect.height; + } + + decodePackbits(data, bytesInThisTile, bdata); + + } else if (compression == COMP_LZW) { + + stream.readFully(data, 0, byteCount); + lzwDecoder.decode(data, bdata, newRect.height); + + } else if (compression == COMP_DEFLATE) { + + stream.readFully(data, 0, byteCount); + inflate(data, bdata); + + } else { + + stream.readFully(bdata, 0, byteCount); + } + + stream.seek(saveOffset); + } catch (IOException ioe) { + throw new RuntimeException(PropertyUtil.getString("TIFFImage13") + ": " + + ioe.getMessage()); + } + } else { // everything else + try { + + if (sampleSize == 8) { + + if (compression == COMP_NONE) { + stream.readFully(bdata, 0, byteCount); + + } else if (compression == COMP_LZW) { + + stream.readFully(data, 0, byteCount); + lzwDecoder.decode(data, bdata, newRect.height); + + } else if (compression == COMP_PACKBITS) { + + stream.readFully(data, 0, byteCount); + decodePackbits(data, unitsInThisTile, bdata); + + } else if (compression == COMP_DEFLATE) { + + stream.readFully(data, 0, byteCount); + inflate(data, bdata); + + } else { + throw new RuntimeException(PropertyUtil.getString("TIFFImage10") + + ": " + compression); + } + + } else if (sampleSize == 16) { + + if (compression == COMP_NONE) { + + readShorts(byteCount / 2, sdata); + + } else if (compression == COMP_LZW) { + + stream.readFully(data, 0, byteCount); + + // Since unitsInThisTile is the number of shorts, + // but we do our decompression in terms of bytes, we + // need to multiply unitsInThisTile by 2 in order to + // figure out how many bytes we'll get after + // decompression. + byte[] byteArray = new byte[unitsInThisTile * 2]; + lzwDecoder.decode(data, byteArray, newRect.height); + interpretBytesAsShorts(byteArray, sdata, + unitsInThisTile); + + } else if (compression == COMP_PACKBITS) { + + stream.readFully(data, 0, byteCount); + + // Since unitsInThisTile is the number of shorts, + // but we do our decompression in terms of bytes, we + // need to multiply unitsInThisTile by 2 in order to + // figure out how many bytes we'll get after + // decompression. + int bytesInThisTile = unitsInThisTile * 2; + + byte[] byteArray = new byte[bytesInThisTile]; + decodePackbits(data, bytesInThisTile, byteArray); + interpretBytesAsShorts(byteArray, sdata, + unitsInThisTile); + } else if (compression == COMP_DEFLATE) { + + stream.readFully(data, 0, byteCount); + byte[] byteArray = new byte[unitsInThisTile * 2]; + inflate(data, byteArray); + interpretBytesAsShorts(byteArray, sdata, + unitsInThisTile); + + } + } else if (sampleSize == 32 + && dataType == DataBuffer.TYPE_INT) { // redundant + if (compression == COMP_NONE) { + + readInts(byteCount / 4, idata); + + } else if (compression == COMP_LZW) { + + stream.readFully(data, 0, byteCount); + + // Since unitsInThisTile is the number of ints, + // but we do our decompression in terms of bytes, we + // need to multiply unitsInThisTile by 4 in order to + // figure out how many bytes we'll get after + // decompression. + byte[] byteArray = new byte[unitsInThisTile * 4]; + lzwDecoder.decode(data, byteArray, newRect.height); + interpretBytesAsInts(byteArray, idata, + unitsInThisTile); + + } else if (compression == COMP_PACKBITS) { + + stream.readFully(data, 0, byteCount); + + // Since unitsInThisTile is the number of ints, + // but we do our decompression in terms of bytes, we + // need to multiply unitsInThisTile by 4 in order to + // figure out how many bytes we'll get after + // decompression. + int bytesInThisTile = unitsInThisTile * 4; + + byte[] byteArray = new byte[bytesInThisTile]; + decodePackbits(data, bytesInThisTile, byteArray); + interpretBytesAsInts(byteArray, idata, + unitsInThisTile); + } else if (compression == COMP_DEFLATE) { + + stream.readFully(data, 0, byteCount); + byte[] byteArray = new byte[unitsInThisTile * 4]; + inflate(data, byteArray); + interpretBytesAsInts(byteArray, idata, + unitsInThisTile); + + } + } + + stream.seek(saveOffset); + + } catch (IOException ioe) { + throw new RuntimeException(PropertyUtil.getString("TIFFImage13") + ": " + + ioe.getMessage()); + } + + // Modify the data for certain special cases. + switch (imageType) { + case TYPE_GRAY: + case TYPE_GRAY_ALPHA: + if (isWhiteZero) { + // Since we are using a ComponentColorModel with this + // image, we need to change the WhiteIsZero data to + // BlackIsZero data so it will display properly. + if (dataType == DataBuffer.TYPE_BYTE + && !(getColorModel() instanceof IndexColorModel)) { + + for (int l = 0; l < bdata.length; l += numBands) { + bdata[l] = (byte)(255 - bdata[l]); + } + } else if (dataType == DataBuffer.TYPE_USHORT) { + + int ushortMax = Short.MAX_VALUE - Short.MIN_VALUE; + for (int l = 0; l < sdata.length; l += numBands) { + sdata[l] = (short)(ushortMax - sdata[l]); + } + + } else if (dataType == DataBuffer.TYPE_SHORT) { + + for (int l = 0; l < sdata.length; l += numBands) { + sdata[l] = (short)(~sdata[l]); + } + } else if (dataType == DataBuffer.TYPE_INT) { + + long uintMax = ((long)Integer.MAX_VALUE + - (long)Integer.MIN_VALUE); + for (int l = 0; l < idata.length; l += numBands) { + idata[l] = (int)(uintMax - idata[l]); + } + } + } + break; + case TYPE_RGB: + // Change RGB to BGR order, as Java2D displays that faster. + // Unnecessary for JPEG-in-TIFF as the decoder handles it. + if (sampleSize == 8 && compression != COMP_JPEG_TTN2) { + for (int i = 0; i < unitsInThisTile; i += 3) { + bswap = bdata[i]; + bdata[i] = bdata[i + 2]; + bdata[i + 2] = bswap; + } + } else if (sampleSize == 16) { + for (int i = 0; i < unitsInThisTile; i += 3) { + sswap = sdata[i]; + sdata[i] = sdata[i + 2]; + sdata[i + 2] = sswap; + } + } else if (sampleSize == 32) { + if (dataType == DataBuffer.TYPE_INT) { + for (int i = 0; i < unitsInThisTile; i += 3) { + iswap = idata[i]; + idata[i] = idata[i + 2]; + idata[i + 2] = iswap; + } + } + } + break; + case TYPE_RGB_ALPHA: + // Convert from RGBA to ABGR for Java2D + if (sampleSize == 8) { + for (int i = 0; i < unitsInThisTile; i += 4) { + // Swap R and A + bswap = bdata[i]; + bdata[i] = bdata[i + 3]; + bdata[i + 3] = bswap; + + // Swap G and B + bswap = bdata[i + 1]; + bdata[i + 1] = bdata[i + 2]; + bdata[i + 2] = bswap; + } + } else if (sampleSize == 16) { + for (int i = 0; i < unitsInThisTile; i += 4) { + // Swap R and A + sswap = sdata[i]; + sdata[i] = sdata[i + 3]; + sdata[i + 3] = sswap; + + // Swap G and B + sswap = sdata[i + 1]; + sdata[i + 1] = sdata[i + 2]; + sdata[i + 2] = sswap; + } + } else if (sampleSize == 32) { + if (dataType == DataBuffer.TYPE_INT) { + for (int i = 0; i < unitsInThisTile; i += 4) { + // Swap R and A + iswap = idata[i]; + idata[i] = idata[i + 3]; + idata[i + 3] = iswap; + + // Swap G and B + iswap = idata[i + 1]; + idata[i + 1] = idata[i + 2]; + idata[i + 2] = iswap; + } + } + } + break; + case TYPE_YCBCR_SUB: + // Post-processing for YCbCr with subsampled chrominance: + // simply replicate the chroma channels for displayability. + int pixelsPerDataUnit = chromaSubH * chromaSubV; + + int numH = newRect.width / chromaSubH; + int numV = newRect.height / chromaSubV; + + byte[] tempData = new byte[numH * numV * (pixelsPerDataUnit + 2)]; + System.arraycopy(bdata, 0, tempData, 0, tempData.length); + + int samplesPerDataUnit = pixelsPerDataUnit * 3; + int[] pixels = new int[samplesPerDataUnit]; + + int bOffset = 0; + int offsetCb = pixelsPerDataUnit; + int offsetCr = offsetCb + 1; + + int y = newRect.y; + for (int j = 0; j < numV; j++) { + int x = newRect.x; + for (int i = 0; i < numH; i++) { + int cb = tempData[bOffset + offsetCb]; + int cr = tempData[bOffset + offsetCr]; + int k = 0; + while (k < samplesPerDataUnit) { + pixels[k++] = tempData[bOffset++]; + pixels[k++] = cb; + pixels[k++] = cr; + } + bOffset += 2; + tile.setPixels(x, y, chromaSubH, chromaSubV, pixels); + x += chromaSubH; + } + y += chromaSubV; + } + + break; + } + } + + return tile; + } + + private void readShorts(int shortCount, short[] shortArray) { + + // Since each short consists of 2 bytes, we need a + // byte array of double size + int byteCount = 2 * shortCount; + byte[] byteArray = new byte[byteCount]; + + try { + stream.readFully(byteArray, 0, byteCount); + } catch (IOException ioe) { + throw new RuntimeException(PropertyUtil.getString("TIFFImage13") + ": " + + ioe.getMessage()); + } + + interpretBytesAsShorts(byteArray, shortArray, shortCount); + } + + private void readInts(int intCount, int[] intArray) { + + // Since each int consists of 4 bytes, we need a + // byte array of quadruple size + int byteCount = 4 * intCount; + byte[] byteArray = new byte[byteCount]; + + try { + stream.readFully(byteArray, 0, byteCount); + } catch (IOException ioe) { + throw new RuntimeException(PropertyUtil.getString("TIFFImage13") + ": " + + ioe.getMessage()); + } + + interpretBytesAsInts(byteArray, intArray, intCount); + } + + // Method to interpret a byte array to a short array, depending on + // whether the bytes are stored in a big endian or little endian format. + private void interpretBytesAsShorts(byte[] byteArray, + short[] shortArray, + int shortCount) { + + int j = 0; + int firstByte; + int secondByte; + + if (isBigEndian) { + + for (int i = 0; i < shortCount; i++) { + firstByte = byteArray[j++] & 0xff; + secondByte = byteArray[j++] & 0xff; + shortArray[i] = (short)((firstByte << 8) + secondByte); + } + + } else { + + for (int i = 0; i < shortCount; i++) { + firstByte = byteArray[j++] & 0xff; + secondByte = byteArray[j++] & 0xff; + shortArray[i] = (short)((secondByte << 8) + firstByte); + } + } + } + + // Method to interpret a byte array to a int array, depending on + // whether the bytes are stored in a big endian or little endian format. + private void interpretBytesAsInts(byte[] byteArray, + int[] intArray, + int intCount) { + + int j = 0; + + if (isBigEndian) { + + for (int i = 0; i < intCount; i++) { + intArray[i] = (((byteArray[j++] & 0xff) << 24) + | ((byteArray[j++] & 0xff) << 16) + | ((byteArray[j++] & 0xff) << 8) + | (byteArray[j++] & 0xff)); + } + + } else { + + for (int i = 0; i < intCount; i++) { + intArray[i] = ((byteArray[j++] & 0xff) + | ((byteArray[j++] & 0xff) << 8) + | ((byteArray[j++] & 0xff) << 16) + | ((byteArray[j++] & 0xff) << 24)); + } + } + } + + // Uncompress packbits compressed image data. + private byte[] decodePackbits(byte[] data, int arraySize, byte[] dst) { + + if (dst == null) { + dst = new byte[arraySize]; + } + + int srcCount = 0; + int dstCount = 0; + byte repeat; + byte b; + + try { + + while (dstCount < arraySize) { + + b = data[srcCount++]; + + if (b >= 0 && b <= 127) { + + // literal run packet + for (int i = 0; i < (b + 1); i++) { + dst[dstCount++] = data[srcCount++]; + } + + } else if (b <= -1 && b >= -127) { + + // 2 byte encoded run packet + repeat = data[srcCount++]; + for (int i = 0; i < (-b + 1); i++) { + dst[dstCount++] = repeat; + } + + } else { + // no-op packet. Do nothing + srcCount++; + } + } + } catch (java.lang.ArrayIndexOutOfBoundsException ae) { + throw new RuntimeException(PropertyUtil.getString("TIFFImage14") + ": " + + ae.getMessage()); + } + + return dst; + } + + // Need a createColorModel(). + // Create ComponentColorModel for TYPE_RGB images + private ComponentColorModel createAlphaComponentColorModel( + int dataType, int numBands, + boolean isAlphaPremultiplied, int transparency) { + + ComponentColorModel ccm = null; + int[] rgbBits = null; + ColorSpace cs = null; + switch(numBands) { + case 2: // gray+alpha + cs = ColorSpace.getInstance(ColorSpace.CS_GRAY); + break; + case 4: // RGB+alpha + cs = ColorSpace.getInstance(ColorSpace.CS_sRGB); + break; + default: + throw new IllegalArgumentException(PropertyUtil.getString("TIFFImage19") + ": " + + numBands); + } + + int componentSize = 0; + switch(dataType) { + case DataBuffer.TYPE_BYTE: + componentSize = 8; + break; + case DataBuffer.TYPE_USHORT: + case DataBuffer.TYPE_SHORT: + componentSize = 16; + break; + case DataBuffer.TYPE_INT: + componentSize = 32; + break; + default: + throw new IllegalArgumentException(PropertyUtil.getString("TIFFImage20") + ": " + + dataType); + } + + rgbBits = new int[numBands]; + for (int i = 0; i < numBands; i++) { + rgbBits[i] = componentSize; + } + + ccm = new ComponentColorModel(cs, + rgbBits, + true, + isAlphaPremultiplied, + transparency, + dataType); + + + return ccm; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/codec/tiff/TIFFImageDecoder.java b/src/main/java/org/apache/xmlgraphics/image/codec/tiff/TIFFImageDecoder.java new file mode 100644 index 0000000..44217ea --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/codec/tiff/TIFFImageDecoder.java @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: TIFFImageDecoder.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.image.codec.tiff; + +import java.awt.image.RenderedImage; +import java.io.IOException; + +import org.apache.xmlgraphics.image.codec.util.ImageDecoderImpl; +import org.apache.xmlgraphics.image.codec.util.PropertyUtil; +import org.apache.xmlgraphics.image.codec.util.SeekableStream; + +/** + * A baseline TIFF reader. The reader has some functionality in addition to + * the baseline specifications for Bilevel images, for which the group 3 and + * group 4 decompression schemes have been implemented. Support for LZW + * decompression has also been added. Support for Horizontal differencing + * predictor decoding is also included, when used with LZW compression. + * However, this support is limited to data with bitsPerSample value of 8. + * When reading in RGB images, support for alpha and extraSamples being + * present has been added. Support for reading in images with 16 bit samples + * has been added. Support for the SampleFormat tag (signed samples as well + * as floating-point samples) has also been added. In all other cases, support + * is limited to Baseline specifications. + * + * + */ +public class TIFFImageDecoder extends ImageDecoderImpl { + + // All the TIFF tags that we care about + public static final int TIFF_IMAGE_WIDTH = 256; + public static final int TIFF_IMAGE_LENGTH = 257; + public static final int TIFF_BITS_PER_SAMPLE = 258; + public static final int TIFF_COMPRESSION = 259; + public static final int TIFF_PHOTOMETRIC_INTERPRETATION = 262; + public static final int TIFF_FILL_ORDER = 266; + public static final int TIFF_STRIP_OFFSETS = 273; + public static final int TIFF_SAMPLES_PER_PIXEL = 277; + public static final int TIFF_ROWS_PER_STRIP = 278; + public static final int TIFF_STRIP_BYTE_COUNTS = 279; + public static final int TIFF_X_RESOLUTION = 282; + public static final int TIFF_Y_RESOLUTION = 283; + public static final int TIFF_PLANAR_CONFIGURATION = 284; + public static final int TIFF_T4_OPTIONS = 292; + public static final int TIFF_T6_OPTIONS = 293; + public static final int TIFF_RESOLUTION_UNIT = 296; + public static final int TIFF_PREDICTOR = 317; + public static final int TIFF_COLORMAP = 320; + public static final int TIFF_TILE_WIDTH = 322; + public static final int TIFF_TILE_LENGTH = 323; + public static final int TIFF_TILE_OFFSETS = 324; + public static final int TIFF_TILE_BYTE_COUNTS = 325; + public static final int TIFF_EXTRA_SAMPLES = 338; + public static final int TIFF_SAMPLE_FORMAT = 339; + public static final int TIFF_S_MIN_SAMPLE_VALUE = 340; + public static final int TIFF_S_MAX_SAMPLE_VALUE = 341; + + public static final int TIFF_ICC_PROFILE = 34675; + + public TIFFImageDecoder(SeekableStream input, TIFFDecodeParam param) { + super(input, param); + } + + public int getNumPages() throws IOException { + return TIFFDirectory.getNumDirectories(input); + } + + public RenderedImage decodeAsRenderedImage(int page) throws IOException { + if ((page < 0) || (page >= getNumPages())) { + throw new IOException(PropertyUtil.getString("TIFFImageDecoder0")); + } + return new TIFFImage(input, (TIFFDecodeParam) param, page); + } +} diff --git a/src/main/java/org/apache/xmlgraphics/image/codec/tiff/TIFFImageEncoder.java b/src/main/java/org/apache/xmlgraphics/image/codec/tiff/TIFFImageEncoder.java new file mode 100644 index 0000000..375aa1e --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/codec/tiff/TIFFImageEncoder.java @@ -0,0 +1,1370 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: TIFFImageEncoder.java 1804124 2017-08-04 14:13:54Z ssteiner $ */ + +package org.apache.xmlgraphics.image.codec.tiff; + +import java.awt.Rectangle; +import java.awt.image.ColorModel; +import java.awt.image.ComponentSampleModel; +import java.awt.image.DataBuffer; +import java.awt.image.DataBufferByte; +import java.awt.image.IndexColorModel; +import java.awt.image.MultiPixelPackedSampleModel; +import java.awt.image.Raster; +import java.awt.image.RenderedImage; +import java.awt.image.SampleModel; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.RandomAccessFile; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.zip.Deflater; + +import org.apache.xmlgraphics.image.codec.util.ImageEncodeParam; +import org.apache.xmlgraphics.image.codec.util.ImageEncoderImpl; +import org.apache.xmlgraphics.image.codec.util.PropertyUtil; +import org.apache.xmlgraphics.image.codec.util.SeekableOutputStream; + +// CSOFF: ConstantName +// CSOFF: MissingSwitchDefault +// CSOFF: MultipleVariableDeclarations +// CSOFF: OperatorWrap +// CSOFF: WhitespaceAround + +/** + * A baseline TIFF writer. The writer outputs TIFF images in either Bilevel, + * Greyscale, Palette color or Full Color modes. + * + */ +public class TIFFImageEncoder extends ImageEncoderImpl { + + // Incidental tags + private static final int TIFF_JPEG_TABLES = 347; + private static final int TIFF_YCBCR_SUBSAMPLING = 530; + private static final int TIFF_YCBCR_POSITIONING = 531; + private static final int TIFF_REF_BLACK_WHITE = 532; + + + + public TIFFImageEncoder(OutputStream output, ImageEncodeParam param) { + super(output, param); + if (this.param == null) { + this.param = new TIFFEncodeParam(); + } + } + + /** + * Encodes a RenderedImage and writes the output to the + * OutputStream associated with this ImageEncoder. + */ + public void encode(RenderedImage im) throws IOException { + // Write the file header (8 bytes). + writeFileHeader(); + + // Get the encoding parameters. + TIFFEncodeParam encodeParam = (TIFFEncodeParam)param; + + Iterator iter = encodeParam.getExtraImages(); + if (iter != null) { + int ifdOffset = 8; + RenderedImage nextImage = im; + TIFFEncodeParam nextParam = encodeParam; + boolean hasNext; + do { + hasNext = iter.hasNext(); + ifdOffset = encode(nextImage, nextParam, ifdOffset, !hasNext); + if (hasNext) { + Object obj = iter.next(); + if (obj instanceof RenderedImage) { + nextImage = (RenderedImage)obj; + nextParam = encodeParam; + } else if (obj instanceof Object[]) { + Object[] o = (Object[])obj; + nextImage = (RenderedImage)o[0]; + nextParam = (TIFFEncodeParam)o[1]; + } + } + } while(hasNext); + } else { + encode(im, encodeParam, 8, true); + } + } + + /** + * Encodes a RenderedImage as part of a multi-page file and writes the output to the + * OutputStream associated with this ImageEncoder. + *

    + * When you sent all pages, make sure you call finishMultiple() in the end. Otherwise, + * the generated file will be corrupted. + * @param context the context object you receive as return value to a previous call to + * encodeMultiple(). Set null for the first image. + * @param img the image + * @return a context object needed for writing multiple pages for a single image file + * @throws IOException In case of an I/O error + */ + public Object encodeMultiple(Object context, RenderedImage img) throws IOException { + // Get the encoding parameters. + TIFFEncodeParam encodeParam = (TIFFEncodeParam)param; + if (encodeParam.getExtraImages() != null) { + throw new IllegalStateException(PropertyUtil.getString("TIFFImageEncoder11")); + } + + Context c = (Context)context; + if (c == null) { + c = new Context(); + // Write the file header (8 bytes). + writeFileHeader(); + } else { + //write image + c.ifdOffset = encode(c.nextImage, encodeParam, c.ifdOffset, false); + } + c.nextImage = img; + return c; + } + + /** + * Signals the encoder that you've finished sending pages for a multi-page image files. + * @param context the context object you receive as return value to a previous call to + * encodeMultiple() + * @throws IOException In case of an I/O error + */ + public void finishMultiple(Object context) throws IOException { + if (context == null) { + throw new NullPointerException(); + } + Context c = (Context)context; + // Get the encoding parameters. + TIFFEncodeParam encodeParam = (TIFFEncodeParam)param; + + //write last image + c.ifdOffset = encode(c.nextImage, encodeParam, c.ifdOffset, true); + } + + private static class Context { + //TODO This approach causes always two images to be present at the same time. + //The encoder has to be changed a little to avoid that. + private RenderedImage nextImage; + private int ifdOffset = 8; //Initial offset + } + + private int encode(RenderedImage im, TIFFEncodeParam encodeParam, + int ifdOffset, boolean isLast) throws IOException { + // Currently all images are stored uncompressed. + CompressionValue compression = encodeParam.getCompression(); + + if (compression == CompressionValue.JPEG_TTN2) { + throw new IllegalArgumentException(PropertyUtil.getString("TIFFImageEncoder12")); + } + + // Get tiled output preference. + boolean isTiled = encodeParam.getWriteTiled(); + + // Set bounds. + int minX = im.getMinX(); + int minY = im.getMinY(); + int width = im.getWidth(); + int height = im.getHeight(); + + // Get SampleModel. + SampleModel sampleModel = im.getSampleModel(); + ColorModel colorModel = im.getColorModel(); + int[] sampleSize = sampleModel.getSampleSize(); + int dataTypeSize = sampleSize[0]; + int numBands = sampleModel.getNumBands(); + int dataType = sampleModel.getDataType(); + validateImage(dataTypeSize, sampleSize, numBands, dataType, colorModel); + + boolean dataTypeIsShort = dataType == DataBuffer.TYPE_SHORT + || dataType == DataBuffer.TYPE_USHORT; + + // Set image type. + ImageInfo imageInfo = ImageInfo.newInstance(im, dataTypeSize, numBands, colorModel, + encodeParam); + + if (imageInfo.getType() == ImageType.UNSUPPORTED) { + throw new RuntimeException(PropertyUtil.getString("TIFFImageEncoder8")); + } + + final int numTiles = imageInfo.getNumTiles(); + final long bytesPerTile = imageInfo.getBytesPerTile(); + final long bytesPerRow = imageInfo.getBytesPerRow(); + final int tileHeight = imageInfo.getTileHeight(); + final int tileWidth = imageInfo.getTileWidth(); + + long[] tileByteCounts = new long[numTiles]; + for (int i = 0; i < numTiles; i++) { + tileByteCounts[i] = bytesPerTile; + } + + if (!isTiled) { + // Last strip may have lesser rows + long lastStripRows = height - (tileHeight * (numTiles - 1)); + tileByteCounts[numTiles - 1] = lastStripRows * bytesPerRow; + } + long totalBytesOfData = bytesPerTile * (numTiles - 1) + tileByteCounts[numTiles - 1]; + long[] tileOffsets = new long[numTiles]; + + // Basic fields - have to be in increasing numerical order. + // ImageWidth 256 + // ImageLength 257 + // BitsPerSample 258 + // Compression 259 + // PhotoMetricInterpretation 262 + // StripOffsets 273 + // RowsPerStrip 278 + // StripByteCounts 279 + // XResolution 282 + // YResolution 283 + // ResolutionUnit 296 + + // Create Directory + SortedSet fields = new TreeSet(); + + // Image Width + fields.add(new TIFFField(TIFFImageDecoder.TIFF_IMAGE_WIDTH, + TIFFField.TIFF_LONG, 1, + new long[] {width})); + + // Image Length + fields.add(new TIFFField(TIFFImageDecoder.TIFF_IMAGE_LENGTH, + TIFFField.TIFF_LONG, 1, + new long[] {height})); + + char [] shortSampleSize = new char[numBands]; + for (int i = 0; i < numBands; i++) { + shortSampleSize[i] = (char) dataTypeSize; + } + fields.add(new TIFFField(TIFFImageDecoder.TIFF_BITS_PER_SAMPLE, + TIFFField.TIFF_SHORT, numBands, + shortSampleSize)); + + fields.add(new TIFFField(TIFFImageDecoder.TIFF_COMPRESSION, + TIFFField.TIFF_SHORT, 1, + new char[] {(char)compression.getValue()})); + + fields.add( + new TIFFField(TIFFImageDecoder.TIFF_PHOTOMETRIC_INTERPRETATION, + TIFFField.TIFF_SHORT, 1, + new char[] {(char) imageInfo.getType().getPhotometricInterpretation()})); + + if (!isTiled) { + fields.add(new TIFFField(TIFFImageDecoder.TIFF_STRIP_OFFSETS, + TIFFField.TIFF_LONG, numTiles, + tileOffsets)); + } + + fields.add(new TIFFField(TIFFImageDecoder.TIFF_SAMPLES_PER_PIXEL, + TIFFField.TIFF_SHORT, 1, + new char[] {(char)numBands})); + + if (!isTiled) { + fields.add(new TIFFField(TIFFImageDecoder.TIFF_ROWS_PER_STRIP, + TIFFField.TIFF_LONG, 1, + new long[] {tileHeight})); + + fields.add(new TIFFField(TIFFImageDecoder.TIFF_STRIP_BYTE_COUNTS, + TIFFField.TIFF_LONG, numTiles, + tileByteCounts)); + } + + if (imageInfo.getColormap() != null) { + fields.add(new TIFFField(TIFFImageDecoder.TIFF_COLORMAP, + TIFFField.TIFF_SHORT, imageInfo.getColormapSize(), + imageInfo.getColormap())); + } + + if (isTiled) { + fields.add(new TIFFField(TIFFImageDecoder.TIFF_TILE_WIDTH, + TIFFField.TIFF_LONG, 1, + new long[] {tileWidth})); + + fields.add(new TIFFField(TIFFImageDecoder.TIFF_TILE_LENGTH, + TIFFField.TIFF_LONG, 1, + new long[] {tileHeight})); + + fields.add(new TIFFField(TIFFImageDecoder.TIFF_TILE_OFFSETS, + TIFFField.TIFF_LONG, numTiles, + tileOffsets)); + + fields.add(new TIFFField(TIFFImageDecoder.TIFF_TILE_BYTE_COUNTS, + TIFFField.TIFF_LONG, numTiles, + tileByteCounts)); + } + + if (imageInfo.getNumberOfExtraSamples() > 0) { + char[] extraSamples = new char[imageInfo.getNumberOfExtraSamples()]; + for (int i = 0; i < imageInfo.getNumberOfExtraSamples(); i++) { + extraSamples[i] = (char) imageInfo.getExtraSamplesType().getValue(); + } + fields.add(new TIFFField(TIFFImageDecoder.TIFF_EXTRA_SAMPLES, + TIFFField.TIFF_SHORT, imageInfo.getNumberOfExtraSamples(), + extraSamples)); + } + + // Data Sample Format Extension fields. + if (dataType != DataBuffer.TYPE_BYTE) { + // SampleFormat + char[] sampleFormat = new char[numBands]; + if (dataType == DataBuffer.TYPE_FLOAT) { + sampleFormat[0] = 3; + } else if (dataType == DataBuffer.TYPE_USHORT) { + sampleFormat[0] = 1; + } else { + sampleFormat[0] = 2; + } + for (int b = 1; b < numBands; b++) { + sampleFormat[b] = sampleFormat[0]; + } + fields.add(new TIFFField(TIFFImageDecoder.TIFF_SAMPLE_FORMAT, + TIFFField.TIFF_SHORT, numBands, + sampleFormat)); + + // NOTE: We don't bother setting the SMinSampleValue and + // SMaxSampleValue fields as these both default to the + // extrema of the respective data types. Probably we should + // check for the presence of the "extrema" property and + // use it if available. + } + + if (imageInfo.getType() == ImageType.YCBCR) { + // YCbCrSubSampling: 2 is the default so we must write 1 as + // we do not (yet) do any subsampling. + char subsampleH = 1; + char subsampleV = 1; + + fields.add(new TIFFField(TIFF_YCBCR_SUBSAMPLING, + TIFFField.TIFF_SHORT, 2, + new char[] {subsampleH, subsampleV})); + + + // YCbCr positioning. + fields.add(new TIFFField(TIFF_YCBCR_POSITIONING, + TIFFField.TIFF_SHORT, 1, + new char[] + {(char) ((compression == CompressionValue.JPEG_TTN2) ? 1 : 2)})); + + // Reference black/white. + long[][] refbw; + refbw = new long[][] // CCIR 601.1 headroom/footroom (presumptive) + {{15, 1}, {235, 1}, {128, 1}, {240, 1}, {128, 1}, {240, 1}}; + + fields.add(new TIFFField(TIFF_REF_BLACK_WHITE, + TIFFField.TIFF_RATIONAL, 6, + refbw)); + } + + // ---- No more automatically generated fields should be added + // after this point. ---- + + // Add extra fields specified via the encoding parameters. + TIFFField[] extraFields = encodeParam.getExtraFields(); + List extantTags = new ArrayList(fields.size()); + for (TIFFField fld : fields) { + extantTags.add(fld.getTag()); + } + + for (TIFFField fld : extraFields) { + Integer tagValue = fld.getTag(); + if (!extantTags.contains(tagValue)) { + fields.add(fld); + extantTags.add(tagValue); + } + } + + // ---- No more fields of any type should be added after this. ---- + + // Determine the size of the IFD which is written after the header + // of the stream or after the data of the previous image in a + // multi-page stream. + int dirSize = getDirectorySize(fields); + + // The first data segment is written after the field overflow + // following the IFD so initialize the first offset accordingly. + tileOffsets[0] = ifdOffset + dirSize; + + // Branch here depending on whether data are being compressed. + // If not, then the IFD is written immediately. + // If so then there are three possibilities: + // A) the OutputStream is a SeekableOutputStream (outCache null); + // B) the OutputStream is not a SeekableOutputStream and a file cache + // is used (outCache non-null, tempFile non-null); + // C) the OutputStream is not a SeekableOutputStream and a memory cache + // is used (outCache non-null, tempFile null). + + OutputStream outCache = null; + byte[] compressBuf = null; + File tempFile = null; + + int nextIFDOffset = 0; + boolean skipByte = false; + + Deflater deflater = null; + boolean jpegRGBToYCbCr = false; + + if (compression == CompressionValue.NONE) { + // Determine the number of bytes of padding necessary between + // the end of the IFD and the first data segment such that the + // alignment of the data conforms to the specification (required + // for uncompressed data only). + int numBytesPadding = 0; + if (dataTypeSize == 16 && tileOffsets[0] % 2 != 0) { + numBytesPadding = 1; + tileOffsets[0]++; + } else if (dataTypeSize == 32 && tileOffsets[0] % 4 != 0) { + numBytesPadding = (int)(4 - tileOffsets[0] % 4); + tileOffsets[0] += numBytesPadding; + } + + // Update the data offsets (which TIFFField stores by reference). + for (int i = 1; i < numTiles; i++) { + tileOffsets[i] = tileOffsets[i - 1] + tileByteCounts[i - 1]; + } + + if (!isLast) { + // Determine the offset of the next IFD. + nextIFDOffset = (int)(tileOffsets[0] + totalBytesOfData); + + // IFD offsets must be on a word boundary. + if ((nextIFDOffset & 0x01) != 0) { + nextIFDOffset++; + skipByte = true; + } + } + + // Write the IFD and field overflow before the image data. + writeDirectory(ifdOffset, fields, nextIFDOffset); + + // Write any padding bytes needed between the end of the IFD + // and the start of the actual image data. + if (numBytesPadding != 0) { + for (int padding = 0; padding < numBytesPadding; padding++) { + output.write((byte)0); + } + } + } else { + // If compressing, the cannot be written yet as the size of the + // data segments is unknown. + + if (output instanceof SeekableOutputStream) { + // Simply seek to the first data segment position. + ((SeekableOutputStream)output).seek(tileOffsets[0]); + } else { + // Cache the original OutputStream. + outCache = output; + + try { + // Attempt to create a temporary file. + tempFile = File.createTempFile("jai-SOS-", ".tmp"); + tempFile.deleteOnExit(); + RandomAccessFile raFile = new RandomAccessFile(tempFile, "rw"); + output = new SeekableOutputStream(raFile); + + // this method is exited! + } catch (IOException e) { + // Allocate memory for the entire image data (!). + output = new ByteArrayOutputStream((int)totalBytesOfData); + } + } + + int bufSize = 0; + switch(compression) { + case PACKBITS: + bufSize = (int) (bytesPerTile + ((bytesPerRow + 127) / 128) * tileHeight); + break; + case DEFLATE: + bufSize = (int) bytesPerTile; + deflater = new Deflater(encodeParam.getDeflateLevel()); + break; + default: + bufSize = 0; + } + if (bufSize != 0) { + compressBuf = new byte[bufSize]; + } + } + + // ---- Writing of actual image data ---- + + // Buffer for up to tileHeight rows of pixels + int[] pixels = null; + float[] fpixels = null; + + // Whether to test for contiguous data. + boolean checkContiguous = + ((dataTypeSize == 1 + && sampleModel instanceof MultiPixelPackedSampleModel + && dataType == DataBuffer.TYPE_BYTE) + || (dataTypeSize == 8 + && sampleModel instanceof ComponentSampleModel)); + + // Also create a buffer to hold tileHeight lines of the + // data to be written to the file, so we can use array writes. + byte[] bpixels = null; + if (compression != CompressionValue.JPEG_TTN2) { + if (dataType == DataBuffer.TYPE_BYTE) { + bpixels = new byte[tileHeight * tileWidth * numBands]; + } else if (dataTypeIsShort) { + bpixels = new byte[2 * tileHeight * tileWidth * numBands]; + } else if (dataType == DataBuffer.TYPE_INT + || dataType == DataBuffer.TYPE_FLOAT) { + bpixels = new byte[4 * tileHeight * tileWidth * numBands]; + } + } + + // Process tileHeight rows at a time + int lastRow = minY + height; + int lastCol = minX + width; + int tileNum = 0; + for (int row = minY; row < lastRow; row += tileHeight) { + int rows = isTiled + ? tileHeight : Math.min(tileHeight, lastRow - row); + int size = rows * tileWidth * numBands; + + for (int col = minX; col < lastCol; col += tileWidth) { + // Grab the pixels + Raster src = + im.getData(new Rectangle(col, row, tileWidth, rows)); + + boolean useDataBuffer = false; + if (compression != CompressionValue.JPEG_TTN2) { // JPEG access Raster + if (checkContiguous) { + if (dataTypeSize == 8) { // 8-bit + ComponentSampleModel csm = + (ComponentSampleModel)src.getSampleModel(); + int[] bankIndices = csm.getBankIndices(); + int[] bandOffsets = csm.getBandOffsets(); + int pixelStride = csm.getPixelStride(); + int lineStride = csm.getScanlineStride(); + + if (pixelStride != numBands + || lineStride != bytesPerRow) { + useDataBuffer = false; + } else { + useDataBuffer = true; + for (int i = 0; + useDataBuffer && i < numBands; + i++) { + if (bankIndices[i] != 0 + || bandOffsets[i] != i) { + useDataBuffer = false; + } + } + } + } else { // 1-bit + MultiPixelPackedSampleModel mpp = + (MultiPixelPackedSampleModel)src.getSampleModel(); + if (mpp.getNumBands() == 1 + && mpp.getDataBitOffset() == 0 + && mpp.getPixelBitStride() == 1) { + useDataBuffer = true; + } + } + } + + if (!useDataBuffer) { + if (dataType == DataBuffer.TYPE_FLOAT) { + fpixels = src.getPixels(col, row, tileWidth, rows, + fpixels); + } else { + pixels = src.getPixels(col, row, tileWidth, rows, + pixels); + } + } + } + + int index; + + int pixel = 0; + int k = 0; + switch (dataTypeSize) { + + case 1: + + if (useDataBuffer) { + byte[] btmp = + ((DataBufferByte)src.getDataBuffer()).getData(); + MultiPixelPackedSampleModel mpp = + (MultiPixelPackedSampleModel)src.getSampleModel(); + int lineStride = mpp.getScanlineStride(); + int inOffset = + mpp.getOffset(col + - src.getSampleModelTranslateX(), + row + - src.getSampleModelTranslateY()); + if (lineStride == bytesPerRow) { + System.arraycopy(btmp, inOffset, + bpixels, 0, + (int) bytesPerRow * rows); + } else { + int outOffset = 0; + for (int j = 0; j < rows; j++) { + System.arraycopy(btmp, inOffset, + bpixels, outOffset, + (int) bytesPerRow); + inOffset += lineStride; + outOffset += bytesPerRow; + } + } + } else { + index = 0; + + // For each of the rows in a strip + for (int i = 0; i < rows; i++) { + + // Write number of pixels exactly divisible by 8 + for (int j = 0; j < tileWidth / 8; j++) { + + pixel = + (pixels[index++] << 7) + | (pixels[index++] << 6) + | (pixels[index++] << 5) + | (pixels[index++] << 4) + | (pixels[index++] << 3) + | (pixels[index++] << 2) + | (pixels[index++] << 1) + | pixels[index++]; + bpixels[k++] = (byte)pixel; + } + + // Write the pixels remaining after division by 8 + if (tileWidth % 8 > 0) { + pixel = 0; + for (int j = 0; j < tileWidth % 8; j++) { + pixel |= (pixels[index++] << (7 - j)); + } + bpixels[k++] = (byte)pixel; + } + } + } + + if (compression == CompressionValue.NONE) { + output.write(bpixels, 0, rows * ((tileWidth + 7) / 8)); + } else if (compression == CompressionValue.PACKBITS) { + int numCompressedBytes = + compressPackBits(bpixels, rows, + bytesPerRow, + compressBuf); + tileByteCounts[tileNum++] = numCompressedBytes; + output.write(compressBuf, 0, numCompressedBytes); + } else if (compression == CompressionValue.DEFLATE) { + int numCompressedBytes = + deflate(deflater, bpixels, compressBuf); + tileByteCounts[tileNum++] = numCompressedBytes; + output.write(compressBuf, 0, numCompressedBytes); + } + + break; + + case 4: + + index = 0; + + // For each of the rows in a strip + for (int i = 0; i < rows; i++) { + + // Write the number of pixels that will fit into an + // even number of nibbles. + for (int j = 0; j < tileWidth / 2; j++) { + pixel = (pixels[index++] << 4) | pixels[index++]; + bpixels[k++] = (byte)pixel; + } + + // Last pixel for odd-length lines + if ((tileWidth & 1) == 1) { + pixel = pixels[index++] << 4; + bpixels[k++] = (byte)pixel; + } + } + + if (compression == CompressionValue.NONE) { + output.write(bpixels, 0, rows * ((tileWidth + 1) / 2)); + } else if (compression == CompressionValue.PACKBITS) { + int numCompressedBytes = + compressPackBits(bpixels, rows, + bytesPerRow, + compressBuf); + tileByteCounts[tileNum++] = numCompressedBytes; + output.write(compressBuf, 0, numCompressedBytes); + } else if (compression == CompressionValue.DEFLATE) { + int numCompressedBytes = + deflate(deflater, bpixels, compressBuf); + tileByteCounts[tileNum++] = numCompressedBytes; + output.write(compressBuf, 0, numCompressedBytes); + } + break; + + case 8: + + if (compression != CompressionValue.JPEG_TTN2) { + if (useDataBuffer) { + byte[] btmp = + ((DataBufferByte)src.getDataBuffer()).getData(); + ComponentSampleModel csm = + (ComponentSampleModel)src.getSampleModel(); + int inOffset = + csm.getOffset(col + - src.getSampleModelTranslateX(), + row + - src.getSampleModelTranslateY()); + int lineStride = csm.getScanlineStride(); + if (lineStride == bytesPerRow) { + System.arraycopy(btmp, + inOffset, + bpixels, 0, + (int) bytesPerRow * rows); + } else { + int outOffset = 0; + for (int j = 0; j < rows; j++) { + System.arraycopy(btmp, inOffset, + bpixels, outOffset, + (int) bytesPerRow); + inOffset += lineStride; + outOffset += bytesPerRow; + } + } + } else { + for (int i = 0; i < size; i++) { + bpixels[i] = (byte)pixels[i]; + } + } + } + + if (compression == CompressionValue.NONE) { + output.write(bpixels, 0, size); + } else if (compression == CompressionValue.PACKBITS) { + int numCompressedBytes = + compressPackBits(bpixels, rows, + bytesPerRow, + compressBuf); + tileByteCounts[tileNum++] = numCompressedBytes; + output.write(compressBuf, 0, numCompressedBytes); + } else if (compression == CompressionValue.DEFLATE) { + int numCompressedBytes = + deflate(deflater, bpixels, compressBuf); + tileByteCounts[tileNum++] = numCompressedBytes; + output.write(compressBuf, 0, numCompressedBytes); + } + break; + + case 16: + + int ls = 0; + for (int i = 0; i < size; i++) { + int value = pixels[i]; + bpixels[ls++] = (byte)((value & 0xff00) >> 8); + bpixels[ls++] = (byte) (value & 0x00ff); + } + + if (compression == CompressionValue.NONE) { + output.write(bpixels, 0, size * 2); + } else if (compression == CompressionValue.PACKBITS) { + int numCompressedBytes = + compressPackBits(bpixels, rows, + bytesPerRow, + compressBuf); + tileByteCounts[tileNum++] = numCompressedBytes; + output.write(compressBuf, 0, numCompressedBytes); + } else if (compression == CompressionValue.DEFLATE) { + int numCompressedBytes = + deflate(deflater, bpixels, compressBuf); + tileByteCounts[tileNum++] = numCompressedBytes; + output.write(compressBuf, 0, numCompressedBytes); + } + break; + + case 32: + if (dataType == DataBuffer.TYPE_INT) { + int li = 0; + for (int i = 0; i < size; i++) { + int value = pixels[i]; + bpixels[li++] = (byte)((value & 0xff000000) >>> 24); + bpixels[li++] = (byte)((value & 0x00ff0000) >>> 16); + bpixels[li++] = (byte)((value & 0x0000ff00) >>> 8); + bpixels[li++] = (byte)(value & 0x000000ff); + } + } else { // DataBuffer.TYPE_FLOAT + int lf = 0; + for (int i = 0; i < size; i++) { + int value = Float.floatToIntBits(fpixels[i]); + bpixels[lf++] = (byte)((value & 0xff000000) >>> 24); + bpixels[lf++] = (byte)((value & 0x00ff0000) >>> 16); + bpixels[lf++] = (byte)((value & 0x0000ff00) >>> 8); + bpixels[lf++] = (byte)(value & 0x000000ff); + } + } + if (compression == CompressionValue.NONE) { + output.write(bpixels, 0, size * 4); + } else if (compression == CompressionValue.PACKBITS) { + int numCompressedBytes = + compressPackBits(bpixels, rows, + bytesPerRow, + compressBuf); + tileByteCounts[tileNum++] = numCompressedBytes; + output.write(compressBuf, 0, numCompressedBytes); + } else if (compression == CompressionValue.DEFLATE) { + int numCompressedBytes = + deflate(deflater, bpixels, compressBuf); + tileByteCounts[tileNum++] = numCompressedBytes; + output.write(compressBuf, 0, numCompressedBytes); + } + break; + default: + break; + } + } + } + + if (compression == CompressionValue.NONE) { + // Write an extra byte for IFD word alignment if needed. + if (skipByte) { + output.write((byte)0); + } + } else { + // Recompute the tile offsets the size of the compressed tiles. + int totalBytes = 0; + for (int i = 1; i < numTiles; i++) { + int numBytes = (int)tileByteCounts[i - 1]; + totalBytes += numBytes; + tileOffsets[i] = tileOffsets[i - 1] + numBytes; + } + totalBytes += (int)tileByteCounts[numTiles - 1]; + + nextIFDOffset = isLast + ? 0 : ifdOffset + dirSize + totalBytes; + if ((nextIFDOffset & 0x01) != 0) { // make it even + nextIFDOffset++; + skipByte = true; + } + + if (outCache == null) { + // Original OutputStream must be a SeekableOutputStream. + + // Write an extra byte for IFD word alignment if needed. + if (skipByte) { + output.write((byte)0); + } + + SeekableOutputStream sos = (SeekableOutputStream)output; + + // Save current position. + long savePos = sos.getFilePointer(); + + // Seek backward to the IFD offset and write IFD. + sos.seek(ifdOffset); + writeDirectory(ifdOffset, fields, nextIFDOffset); + + // Seek forward to position after data. + sos.seek(savePos); + } else if (tempFile != null) { + + // Using a file cache for the image data. + + // Open a FileInputStream from which to copy the data. + FileInputStream fileStream = new FileInputStream(tempFile); + try { + // Close the original SeekableOutputStream. + output.close(); + + // Reset variable to the original OutputStream. + output = outCache; + + // Write the IFD. + writeDirectory(ifdOffset, fields, nextIFDOffset); + + // Write the image data. + byte[] copyBuffer = new byte[8192]; + int bytesCopied = 0; + while (bytesCopied < totalBytes) { + int bytesRead = fileStream.read(copyBuffer); + if (bytesRead == -1) { + break; + } + output.write(copyBuffer, 0, bytesRead); + bytesCopied += bytesRead; + } + } finally { + // Delete the temporary file. + fileStream.close(); + } + boolean isDeleted = tempFile.delete(); + assert isDeleted; + + // Write an extra byte for IFD word alignment if needed. + if (skipByte) { + output.write((byte)0); + } + } else if (output instanceof ByteArrayOutputStream) { + + // Using a memory cache for the image data. + + ByteArrayOutputStream memoryStream = (ByteArrayOutputStream)output; + + // Reset variable to the original OutputStream. + output = outCache; + + // Write the IFD. + writeDirectory(ifdOffset, fields, nextIFDOffset); + + // Write the image data. + memoryStream.writeTo(output); + + // Write an extra byte for IFD word alignment if needed. + if (skipByte) { + output.write((byte)0); + } + } else { + // This should never happen. + throw new IllegalStateException(PropertyUtil.getString("TIFFImageEncoder13")); + } + } + + + return nextIFDOffset; + } + + private void validateImage(int dataTypeSize, int[] sampleSize, int numBands, int dataType, + ColorModel colorModel) { + // Retrieve and verify sample size. + for (int i = 1; i < sampleSize.length; i++) { + if (sampleSize[i] != dataTypeSize) { + throw new RuntimeException(PropertyUtil.getString("TIFFImageEncoder0")); + } + } + + // Check low bit limits. + if ((dataTypeSize == 1 || dataTypeSize == 4) && numBands != 1) { + throw new RuntimeException(PropertyUtil.getString("TIFFImageEncoder1")); + } + + // Retrieve and verify data type. + switch (dataType) { + case DataBuffer.TYPE_BYTE: + if (dataTypeSize == 4) { + throw new RuntimeException(PropertyUtil.getString("TIFFImageEncoder2")); + } + break; + case DataBuffer.TYPE_SHORT: + case DataBuffer.TYPE_USHORT: + if (dataTypeSize != 16) { + throw new RuntimeException(PropertyUtil.getString("TIFFImageEncoder3")); + } + break; + case DataBuffer.TYPE_INT: + case DataBuffer.TYPE_FLOAT: + if (dataTypeSize != 32) { + throw new RuntimeException(PropertyUtil.getString("TIFFImageEncoder4")); + } + break; + default: + throw new RuntimeException(PropertyUtil.getString("TIFFImageEncoder5")); + } + + if (colorModel instanceof IndexColorModel && dataType != DataBuffer.TYPE_BYTE) { + // Don't support (unsigned) short palette-color images. + throw new RuntimeException(PropertyUtil.getString("TIFFImageEncoder6")); + } + } + + /** + * Calculates the size of the IFD. + */ + private int getDirectorySize(SortedSet fields) { + // Get the number of entries. + int numEntries = fields.size(); + + // Initialize the size excluding that of any values > 4 bytes. + int dirSize = 2 + numEntries * 12 + 4; + + // Loop over fields adding the size of all values > 4 bytes. + for (Object field1 : fields) { + // Get the field. + TIFFField field = (TIFFField) field1; + + // Determine the size of the field value. + int valueSize = field.getCount() * SIZE_OF_TYPE[field.getType()]; + + // Add any excess size. + if (valueSize > 4) { + dirSize += valueSize; + } + } + + return dirSize; + } + + private void writeFileHeader() throws IOException { + // 8 byte image file header + + // Byte order used within the file - Big Endian + output.write('M'); + output.write('M'); + + // Magic value + output.write(0); + output.write(42); + + // Offset in bytes of the first IFD. + writeLong(8); + } + + private void writeDirectory(int thisIFDOffset, SortedSet fields, + int nextIFDOffset) + throws IOException { + + // 2 byte count of number of directory entries (fields) + int numEntries = fields.size(); + + long offsetBeyondIFD = thisIFDOffset + 12 * numEntries + 4 + 2; + List tooBig = new ArrayList(); + + // Write number of fields in the IFD + writeUnsignedShort(numEntries); + + for (Object field1 : fields) { + + // 12 byte field entry TIFFField + TIFFField field = (TIFFField) field1; + + // byte 0-1 Tag that identifies a field + int tag = field.getTag(); + writeUnsignedShort(tag); + + // byte 2-3 The field type + int type = field.getType(); + writeUnsignedShort(type); + + // bytes 4-7 the number of values of the indicated type except + // ASCII-valued fields which require the total number of bytes. + int count = field.getCount(); + int valueSize = getValueSize(field); + writeLong(type == TIFFField.TIFF_ASCII ? valueSize : count); + + // bytes 8 - 11 the value or value offset + if (valueSize > 4) { + + // We need an offset as data won't fit into 4 bytes + writeLong(offsetBeyondIFD); + offsetBeyondIFD += valueSize; + tooBig.add(field); + + } else { + writeValuesAsFourBytes(field); + } + + } + + // Address of next IFD + writeLong(nextIFDOffset); + + // Write the tag values that did not fit into 4 bytes + for (Object aTooBig : tooBig) { + writeValues((TIFFField) aTooBig); + } + } + + /** + * Determine the number of bytes in the value portion of the field. + */ + private static int getValueSize(TIFFField field) throws UnsupportedEncodingException { + int type = field.getType(); + int count = field.getCount(); + int valueSize = 0; + if (type == TIFFField.TIFF_ASCII) { + for (int i = 0; i < count; i++) { + byte[] stringBytes = field.getAsString(i).getBytes("UTF-8"); // note: default encoding @work here! + valueSize += stringBytes.length; + if (stringBytes[stringBytes.length - 1] != 0) { + valueSize++; + } + } + } else { + valueSize = count * SIZE_OF_TYPE[type]; + } + return valueSize; + } + + private static final int[] SIZE_OF_TYPE = { + 0, // 0 = n/a + 1, // 1 = byte + 1, // 2 = ascii + 2, // 3 = short + 4, // 4 = long + 8, // 5 = rational + 1, // 6 = sbyte + 1, // 7 = undefined + 2, // 8 = sshort + 4, // 9 = slong + 8, // 10 = srational + 4, // 11 = float + 8 // 12 = double + }; + + private void writeValuesAsFourBytes(TIFFField field) throws IOException { + + int dataType = field.getType(); + int count = field.getCount(); + + switch (dataType) { + + // unsigned 8 bits + case TIFFField.TIFF_BYTE: + byte[] bytes = field.getAsBytes(); + if (count > 4) { + count = 4; + } + for (int i = 0; i < count; i++) { + output.write(bytes[i]); + } + + for (int i = 0; i < (4 - count); i++) { + output.write(0); + } + break; + + // unsigned 16 bits + case TIFFField.TIFF_SHORT: + char[] chars = field.getAsChars(); + if (count > 2) { + count = 2; + } + for (int i = 0; i < count; i++) { + writeUnsignedShort(chars[i]); + } + for (int i = 0; i < (2 - count); i++) { + writeUnsignedShort(0); + } + + break; + + // unsigned 32 bits + case TIFFField.TIFF_LONG: + long[] longs = field.getAsLongs(); + + for (int i = 0; i < count; i++) { + writeLong(longs[i]); + } + break; + } + + } + + private void writeValues(TIFFField field) throws IOException { + + int dataType = field.getType(); + int count = field.getCount(); + + switch (dataType) { + + // unsigned 8 bits + case TIFFField.TIFF_BYTE: + case TIFFField.TIFF_SBYTE: + case TIFFField.TIFF_UNDEFINED: + byte[] bytes = field.getAsBytes(); + for (int i = 0; i < count; i++) { + output.write(bytes[i]); + } + break; + + // unsigned 16 bits + case TIFFField.TIFF_SHORT: + char[] chars = field.getAsChars(); + for (int i = 0; i < count; i++) { + writeUnsignedShort(chars[i]); + } + break; + case TIFFField.TIFF_SSHORT: + short[] shorts = field.getAsShorts(); + for (int i = 0; i < count; i++) { + writeUnsignedShort(shorts[i]); + } + break; + + // unsigned 32 bits + case TIFFField.TIFF_LONG: + case TIFFField.TIFF_SLONG: + long[] longs = field.getAsLongs(); + for (int i = 0; i < count; i++) { + writeLong(longs[i]); + } + break; + + case TIFFField.TIFF_FLOAT: + float[] floats = field.getAsFloats(); + for (int i = 0; i < count; i++) { + int intBits = Float.floatToIntBits(floats[i]); + writeLong(intBits); + } + break; + + case TIFFField.TIFF_DOUBLE: + double[] doubles = field.getAsDoubles(); + for (int i = 0; i < count; i++) { + long longBits = Double.doubleToLongBits(doubles[i]); + writeLong(longBits >>> 32); // write upper 32 bits + writeLong(longBits & 0xffffffffL); // write lower 32 bits + } + break; + + case TIFFField.TIFF_RATIONAL: + case TIFFField.TIFF_SRATIONAL: + long[][] rationals = field.getAsRationals(); + for (int i = 0; i < count; i++) { + writeLong(rationals[i][0]); + writeLong(rationals[i][1]); + } + break; + + case TIFFField.TIFF_ASCII: + for (int i = 0; i < count; i++) { + byte[] stringBytes = field.getAsString(i).getBytes("UTF-8"); + output.write(stringBytes); + if (stringBytes[stringBytes.length - 1] != (byte)0) { + output.write((byte)0); + } + } + break; + + default: + throw new RuntimeException(PropertyUtil.getString("TIFFImageEncoder10")); + + } + + } + + // Here s is never expected to have value greater than what can be + // stored in 2 bytes. + private void writeUnsignedShort(int s) throws IOException { + output.write((s & 0xff00) >>> 8); + output.write(s & 0x00ff); + } + + /** + * despite its name, this method writes only 4 bytes to output. + * @param l 32bits of this are written as 4 bytes + * @throws IOException + */ + private void writeLong(long l) throws IOException { + output.write((int)((l & 0xff000000) >>> 24)); + output.write((int)((l & 0x00ff0000) >>> 16)); + output.write((int)((l & 0x0000ff00) >>> 8)); + output.write((int) (l & 0x000000ff)); + } + +// /** +// * Returns the current offset in the supplied OutputStream. +// * This method should only be used if compressing data. +// */ +// private long getOffset(OutputStream out) throws IOException { +// if (out instanceof ByteArrayOutputStream) { +// return ((ByteArrayOutputStream)out).size(); +// } else if (out instanceof SeekableOutputStream) { +// return ((SeekableOutputStream)out).getFilePointer(); +// } else { +// // Shouldn't happen. +// throw new IllegalStateException(PropertyUtil.getString("TIFFImageEncoder13")); +// } +// } + + /** + * Performs PackBits compression on a tile of data. + */ + private static int compressPackBits(byte[] data, int numRows, + long bytesPerRow, byte[] compData) { + int inOffset = 0; + int outOffset = 0; + + for (int i = 0; i < numRows; i++) { + outOffset = packBits(data, inOffset, (int) bytesPerRow, + compData, outOffset); + inOffset += bytesPerRow; + } + + return outOffset; + } + + /** + * Performs PackBits compression for a single buffer of data. + * This should be called for each row of each tile. The returned + * value is the offset into the output buffer after compression. + */ + private static int packBits(byte[] input, int inOffset, int inCount, + byte[] output, int outOffset) { + int inMax = inOffset + inCount - 1; + int inMaxMinus1 = inMax - 1; + + while (inOffset <= inMax) { + int run = 1; + byte replicate = input[inOffset]; + while (run < 127 && inOffset < inMax + && input[inOffset] == input[inOffset + 1]) { + run++; + inOffset++; + } + if (run > 1) { + inOffset++; + output[outOffset++] = (byte)(-(run - 1)); + output[outOffset++] = replicate; + } + + run = 0; + int saveOffset = outOffset; + while (run < 128 + && ((inOffset < inMax + && input[inOffset] != input[inOffset + 1]) + || (inOffset < inMaxMinus1 + && input[inOffset] != input[inOffset + 2]))) { + run++; + output[++outOffset] = input[inOffset++]; + } + if (run > 0) { + output[saveOffset] = (byte)(run - 1); + outOffset++; + } + + if (inOffset == inMax) { + if (run > 0 && run < 128) { + output[saveOffset]++; + output[outOffset++] = input[inOffset++]; + } else { + output[outOffset++] = (byte)0; + output[outOffset++] = input[inOffset++]; + } + } + } + + return outOffset; + } + + private static int deflate(Deflater deflater, + byte[] inflated, byte[] deflated) { + deflater.setInput(inflated); + deflater.finish(); + int numCompressedBytes = deflater.deflate(deflated); + deflater.reset(); + return numCompressedBytes; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/codec/tiff/TIFFLZWDecoder.java b/src/main/java/org/apache/xmlgraphics/image/codec/tiff/TIFFLZWDecoder.java new file mode 100644 index 0000000..660b5e2 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/codec/tiff/TIFFLZWDecoder.java @@ -0,0 +1,253 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: TIFFLZWDecoder.java 1804124 2017-08-04 14:13:54Z ssteiner $ */ + +package org.apache.xmlgraphics.image.codec.tiff; + +import org.apache.xmlgraphics.image.codec.util.PropertyUtil; + +// CSOFF: InnerAssignment +// CSOFF: MultipleVariableDeclarations +// CSOFF: OneStatementPerLine +// CSOFF: OperatorWrap +// CSOFF: WhitespaceAround + +/** + * A class for performing LZW decoding. + */ +public class TIFFLZWDecoder { + + byte[][] stringTable; + byte[] data; + byte[] uncompData; + int tableIndex; + int bitsToGet = 9; + int bytePointer; +// int bitPointer; + int dstIndex; + int w; + int predictor; + int samplesPerPixel; + int nextData; + int nextBits; + + int[] andTable = { + 511, + 1023, + 2047, + 4095 + }; + + public TIFFLZWDecoder(int w, int predictor, int samplesPerPixel) { + this.w = w; + this.predictor = predictor; + this.samplesPerPixel = samplesPerPixel; + } + + /** + * Method to decode LZW compressed data. + * + * @param data The compressed data. + * @param uncompData Array to return the uncompressed data in. + * @param h The number of rows the compressed data contains. + */ + public byte[] decode(byte[] data, byte[] uncompData, int h) { + + if (data[0] == (byte)0x00 && data[1] == (byte)0x01) { + throw new UnsupportedOperationException(PropertyUtil.getString("TIFFLZWDecoder0")); + } + + initializeStringTable(); + + this.data = data; +// this.h = h; + this.uncompData = uncompData; + + // Initialize pointers + bytePointer = 0; +// bitPointer = 0; + dstIndex = 0; + + + nextData = 0; + nextBits = 0; + + int code; + int oldCode = 0; + byte[] string; + + while ((code = getNextCode()) != 257 + && dstIndex != uncompData.length) { + + if (code == 256) { + + initializeStringTable(); + code = getNextCode(); + + if (code == 257) { + break; + } + + writeString(stringTable[code]); + oldCode = code; + + } else { + + if (code < tableIndex) { + + string = stringTable[code]; + + writeString(string); + addStringToTable(stringTable[oldCode], string[0]); + oldCode = code; + + } else { + + string = stringTable[oldCode]; + string = composeString(string, string[0]); + writeString(string); + addStringToTable(string); + oldCode = code; + } + + } + + } + + // Horizontal Differencing Predictor + if (predictor == 2) { + + int count; + for (int j = 0; j < h; j++) { + + count = samplesPerPixel * (j * w + 1); + + for (int i = samplesPerPixel; i < w * samplesPerPixel; i++) { + + uncompData[count] += uncompData[count - samplesPerPixel]; + count++; + } + } + } + + return uncompData; + } + + + /** + * Initialize the string table. + */ + public void initializeStringTable() { + + stringTable = new byte[4096][]; + + for (int i = 0; i < 256; i++) { + stringTable[i] = new byte[1]; + stringTable[i][0] = (byte)i; + } + + tableIndex = 258; + bitsToGet = 9; + } + + /** + * Write out the string just uncompressed. + */ + public void writeString(byte[] string) { + + for (byte aString : string) { + uncompData[dstIndex++] = aString; + } + } + + /** + * Add a new string to the string table. + */ + public void addStringToTable(byte[] oldString, byte newString) { + int length = oldString.length; + byte[] string = new byte[length + 1]; + System.arraycopy(oldString, 0, string, 0, length); + string[length] = newString; + + // Add this new String to the table + stringTable[tableIndex++] = string; + + if (tableIndex == 511) { + bitsToGet = 10; + } else if (tableIndex == 1023) { + bitsToGet = 11; + } else if (tableIndex == 2047) { + bitsToGet = 12; + } + } + + /** + * Add a new string to the string table. + */ + public void addStringToTable(byte[] string) { + + // Add this new String to the table + stringTable[tableIndex++] = string; + + if (tableIndex == 511) { + bitsToGet = 10; + } else if (tableIndex == 1023) { + bitsToGet = 11; + } else if (tableIndex == 2047) { + bitsToGet = 12; + } + } + + /** + * Append newString to the end of oldString. + */ + public byte[] composeString(byte[] oldString, byte newString) { + int length = oldString.length; + byte[] string = new byte[length + 1]; + System.arraycopy(oldString, 0, string, 0, length); + string[length] = newString; + + return string; + } + + // Returns the next 9, 10, 11 or 12 bits + public int getNextCode() { + // Attempt to get the next code. The exception is caught to make + // this robust to cases wherein the EndOfInformation code has been + // omitted from a strip. Examples of such cases have been observed + // in practice. + try { + nextData = (nextData << 8) | (data[bytePointer++] & 0xff); + nextBits += 8; + + if (nextBits < bitsToGet) { + nextData = (nextData << 8) | (data[bytePointer++] & 0xff); + nextBits += 8; + } + + int code = + (nextData >> (nextBits - bitsToGet)) & andTable[bitsToGet - 9]; + nextBits -= bitsToGet; + + return code; + } catch (ArrayIndexOutOfBoundsException e) { + // Strip not terminated as expected: return EndOfInformation code. + return 257; + } + } +} diff --git a/src/main/java/org/apache/xmlgraphics/image/codec/tiff/package.html b/src/main/java/org/apache/xmlgraphics/image/codec/tiff/package.html new file mode 100644 index 0000000..417f567 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/codec/tiff/package.html @@ -0,0 +1,23 @@ + + + +org.apache.xmlgraphics.image.codec.tiff Package + +

    Contains a TIFF image codec.

    + + \ No newline at end of file diff --git a/src/main/java/org/apache/xmlgraphics/image/codec/util/FileCacheSeekableStream.java b/src/main/java/org/apache/xmlgraphics/image/codec/util/FileCacheSeekableStream.java new file mode 100644 index 0000000..86f625b --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/codec/util/FileCacheSeekableStream.java @@ -0,0 +1,262 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: FileCacheSeekableStream.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.image.codec.util; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; + +/** + * A subclass of SeekableStream that may be used to wrap + * a regular InputStream. Seeking backwards is supported + * by means of a file cache. In circumstances that do not allow the + * creation of a temporary file (for example, due to security + * consideration or the absence of local disk), the + * MemoryCacheSeekableStream class may be used instead. + * + *

    The mark() and reset() methods are + * supported. + * + *

    This class is not a committed part of the JAI API. It may + * be removed or changed in future releases of JAI. + */ +public final class FileCacheSeekableStream extends SeekableStream { + + /** The source stream. */ + private InputStream stream; + + /** The cache File. */ + private File cacheFile; + + /** The cache as a RandomAcessFile. */ + private RandomAccessFile cache; + + /** The length of the read buffer. */ + private int bufLen = 1024; + + /** The read buffer. */ + private byte[] buf = new byte[bufLen]; + + /** Number of bytes in the cache. */ + private long length; + + /** Next byte to be read. */ + private long pointer; + + /** True if we've encountered the end of the source stream. */ + private boolean foundEOF; + + /** + * Constructs a MemoryCacheSeekableStream that takes + * its source data from a regular InputStream. + * Seeking backwards is supported by means of an file cache. + * + *

    An IOException will be thrown if the + * attempt to create the cache file fails for any reason. + */ + public FileCacheSeekableStream(InputStream stream) + throws IOException { + this.stream = stream; + this.cacheFile = File.createTempFile("jai-FCSS-", ".tmp"); + cacheFile.deleteOnExit(); + this.cache = new RandomAccessFile(cacheFile, "rw"); + } + + /** + * Ensures that at least pos bytes are cached, + * or the end of the source is reached. The return value + * is equal to the smaller of pos and the + * length of the source file. + */ + private long readUntil(long pos) throws IOException { + // We've already got enough data cached + if (pos < length) { + return pos; + } + // pos >= length but length isn't getting any bigger, so return it + if (foundEOF) { + return length; + } + + long len = pos - length; + cache.seek(length); + while (len > 0) { + // Copy a buffer's worth of data from the source to the cache + // bufLen will always fit into an int so this is safe + int nbytes = stream.read(buf, 0, (int)Math.min(len, bufLen)); + if (nbytes == -1) { + foundEOF = true; + return length; + } + + cache.setLength(cache.length() + nbytes); + cache.write(buf, 0, nbytes); + len -= nbytes; + length += nbytes; + } + + return pos; + } + + /** + * Returns true since all + * FileCacheSeekableStream instances support seeking + * backwards. + */ + public boolean canSeekBackwards() { + return true; + } + + /** + * Returns the current offset in this file. + * + * @return the offset from the beginning of the file, in bytes, + * at which the next read occurs. + */ + public long getFilePointer() { + return pointer; + } + + /** + * Sets the file-pointer offset, measured from the beginning of this + * file, at which the next read occurs. + * + * @param pos the offset position, measured in bytes from the + * beginning of the file, at which to set the file + * pointer. + * @exception IOException if pos is less than + * 0 or if an I/O error occurs. + */ + public void seek(long pos) throws IOException { + if (pos < 0) { + throw new IOException(PropertyUtil.getString("FileCacheSeekableStream0")); + } + pointer = pos; + } + + /** + * Reads the next byte of data from the input stream. The value byte is + * returned as an int in the range 0 to + * 255. If no byte is available because the end of the stream + * has been reached, the value -1 is returned. This method + * blocks until input data is available, the end of the stream is detected, + * or an exception is thrown. + * + * @return the next byte of data, or -1 if the end of the + * stream is reached. + * @exception IOException if an I/O error occurs. + */ + public int read() throws IOException { + long next = pointer + 1; + long pos = readUntil(next); + if (pos >= next) { + cache.seek(pointer++); + return cache.read(); + } else { + return -1; + } + } + + /** + * Reads up to len bytes of data from the input stream into + * an array of bytes. An attempt is made to read as many as + * len bytes, but a smaller number may be read, possibly + * zero. The number of bytes actually read is returned as an integer. + * + *

    This method blocks until input data is available, end of file is + * detected, or an exception is thrown. + * + *

    If b is null, a + * NullPointerException is thrown. + * + *

    If off is negative, or len is negative, or + * off+len is greater than the length of the array + * b, then an IndexOutOfBoundsException is + * thrown. + * + *

    If len is zero, then no bytes are read and + * 0 is returned; otherwise, there is an attempt to read at + * least one byte. If no byte is available because the stream is at end of + * file, the value -1 is returned; otherwise, at least one + * byte is read and stored into b. + * + *

    The first byte read is stored into element b[off], the + * next one into b[off+1], and so on. The number of bytes read + * is, at most, equal to len. Let k be the number of + * bytes actually read; these bytes will be stored in elements + * b[off] through b[off+k-1], + * leaving elements b[off+k] through + * b[off+len-1] unaffected. + * + *

    In every case, elements b[0] through + * b[off] and elements b[off+len] through + * b[b.length-1] are unaffected. + * + *

    If the first byte cannot be read for any reason other than end of + * file, then an IOException is thrown. In particular, an + * IOException is thrown if the input stream has been closed. + * + * @param b the buffer into which the data is read. + * @param off the start offset in array b + * at which the data is written. + * @param len the maximum number of bytes to read. + * @return the total number of bytes read into the buffer, or + * -1 if there is no more data because the end of + * the stream has been reached. + * @exception IOException if an I/O error occurs. + */ + public int read(byte[] b, int off, int len) throws IOException { + if (b == null) { + throw new NullPointerException(); + } + if ((off < 0) || (len < 0) || (off + len > b.length)) { + throw new IndexOutOfBoundsException(); + } + if (len == 0) { + return 0; + } + + long pos = readUntil(pointer + len); + + // len will always fit into an int so this is safe + len = (int)Math.min(len, pos - pointer); + if (len > 0) { + cache.seek(pointer); + cache.readFully(b, off, len); + pointer += len; + return len; + } else { + return -1; + } + } + + /** + * Closes this stream and releases any system resources + * associated with the stream. + * + * @throws IOException if an I/O error occurs. + */ + public void close() throws IOException { + super.close(); + cache.close(); + cacheFile.delete(); + } +} diff --git a/src/main/java/org/apache/xmlgraphics/image/codec/util/ForwardSeekableStream.java b/src/main/java/org/apache/xmlgraphics/image/codec/util/ForwardSeekableStream.java new file mode 100644 index 0000000..cc5737a --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/codec/util/ForwardSeekableStream.java @@ -0,0 +1,128 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ForwardSeekableStream.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.image.codec.util; + +import java.io.IOException; +import java.io.InputStream; + +/** + * A subclass of SeekableStream that may be used + * to wrap a regular InputStream efficiently. + * Seeking backwards is not supported. + * + */ +public class ForwardSeekableStream extends SeekableStream { + + /** The source InputStream. */ + private InputStream src; + + /** The current position. */ + long pointer; + + /** + * Constructs a InputStreamForwardSeekableStream from a + * regular InputStream. + */ + public ForwardSeekableStream(InputStream src) { + this.src = src; + } + + /** Forwards the request to the real InputStream. */ + public final int read() throws IOException { + int result = src.read(); + if (result != -1) { + ++pointer; + } + return result; + } + + /** Forwards the request to the real InputStream. */ + public final int read(byte[] b, int off, int len) throws IOException { + int result = src.read(b, off, len); + if (result != -1) { + pointer += result; + } + return result; + } + + /** Forwards the request to the real InputStream. */ + public final long skip(long n) throws IOException { + long skipped = src.skip(n); + pointer += skipped; + return skipped; + } + + /** Forwards the request to the real InputStream. */ + public final int available() throws IOException { + return src.available(); + } + + /** Forwards the request to the real InputStream. */ + public final void close() throws IOException { + src.close(); + } + + /** + * Forwards the request to the real InputStream. + * We use {@link SeekableStream#markPos} + */ + public final synchronized void mark(int readLimit) { + markPos = pointer; + src.mark(readLimit); + } + + /** + * Forwards the request to the real InputStream. + * We use {@link SeekableStream#markPos} + */ + public final synchronized void reset() throws IOException { + if (markPos != -1) { + pointer = markPos; + } + src.reset(); + } + + /** Forwards the request to the real InputStream. */ + public boolean markSupported() { + return src.markSupported(); + } + + /** Returns false since seking backwards is not supported. */ + public final boolean canSeekBackwards() { + return false; + } + + /** Returns the current position in the stream (bytes read). */ + public final long getFilePointer() { + return pointer; + } + + /** + * Seeks forward to the given position in the stream. + * If pos is smaller than the current position + * as returned by getFilePointer(), nothing + * happens. + */ + public final void seek(long pos) throws IOException { + while (pos - pointer > 0) { + pointer += src.skip(pos - pointer); + } + } +} diff --git a/src/main/java/org/apache/xmlgraphics/image/codec/util/ImageDecodeParam.java b/src/main/java/org/apache/xmlgraphics/image/codec/util/ImageDecodeParam.java new file mode 100644 index 0000000..e3fdd46 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/codec/util/ImageDecodeParam.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageDecodeParam.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.image.codec.util; + +import java.io.Serializable; + +/** + * An empty (marker) interface to be implemented by all image decoder + * parameter classes. + * + *

    This interface is not a committed part of the JAI API. It may + * be removed or changed in future releases of JAI. + */ +public interface ImageDecodeParam extends Cloneable, Serializable { +} diff --git a/src/main/java/org/apache/xmlgraphics/image/codec/util/ImageDecoder.java b/src/main/java/org/apache/xmlgraphics/image/codec/util/ImageDecoder.java new file mode 100644 index 0000000..17491e7 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/codec/util/ImageDecoder.java @@ -0,0 +1,93 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageDecoder.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.image.codec.util; + +import java.awt.image.Raster; +import java.awt.image.RenderedImage; +import java.io.IOException; + +/** + * An interface describing objects that transform an InputStream into a + * BufferedImage or Raster. + * + */ +public interface ImageDecoder { + + /** + * Returns the current parameters as an instance of the + * ImageDecodeParam interface. Concrete implementations of this + * interface will return corresponding concrete implementations of + * the ImageDecodeParam interface. For example, a JPEGImageDecoder + * will return an instance of JPEGDecodeParam. + */ + ImageDecodeParam getParam(); + + /** + * Sets the current parameters to an instance of the + * ImageDecodeParam interface. Concrete implementations + * of ImageDecoder may throw a RuntimeException if the + * param argument is not an instance of the appropriate + * subclass or subinterface. For example, a JPEGImageDecoder + * will expect param to be an instance of JPEGDecodeParam. + */ + void setParam(ImageDecodeParam param); + + /** Returns the SeekableStream associated with this ImageDecoder. */ + SeekableStream getInputStream(); + + /** Returns the number of pages present in the current stream. */ + int getNumPages() throws IOException; + + /** + * Returns a Raster that contains the decoded contents of the + * SeekableStream associated with this ImageDecoder. Only + * the first page of a multi-page image is decoded. + */ + Raster decodeAsRaster() throws IOException; + + /** + * Returns a Raster that contains the decoded contents of the + * SeekableStream associated with this ImageDecoder. + * The given page of a multi-page image is decoded. If + * the page does not exist, an IOException will be thrown. + * Page numbering begins at zero. + * + * @param page The page to be decoded. + */ + Raster decodeAsRaster(int page) throws IOException; + + /** + * Returns a RenderedImage that contains the decoded contents of the + * SeekableStream associated with this ImageDecoder. Only + * the first page of a multi-page image is decoded. + */ + RenderedImage decodeAsRenderedImage() throws IOException; + + /** + * Returns a RenderedImage that contains the decoded contents of the + * SeekableStream associated with this ImageDecoder. + * The given page of a multi-page image is decoded. If + * the page does not exist, an IOException will be thrown. + * Page numbering begins at zero. + * + * @param page The page to be decoded. + */ + RenderedImage decodeAsRenderedImage(int page) throws IOException; +} diff --git a/src/main/java/org/apache/xmlgraphics/image/codec/util/ImageDecoderImpl.java b/src/main/java/org/apache/xmlgraphics/image/codec/util/ImageDecoderImpl.java new file mode 100644 index 0000000..07b1dcf --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/codec/util/ImageDecoderImpl.java @@ -0,0 +1,162 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageDecoderImpl.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.image.codec.util; + +import java.awt.image.Raster; +import java.awt.image.RenderedImage; +import java.io.IOException; +import java.io.InputStream; + +/** + * A partial implementation of the ImageDecoder interface + * useful for subclassing. + * + */ +public abstract class ImageDecoderImpl implements ImageDecoder { + + /** + * The SeekableStream associcted with this + * ImageEncoder. + */ + protected SeekableStream input; + + /** + * The ImageDecodeParam object associated with this + * ImageEncoder. + */ + protected ImageDecodeParam param; + + /** + * Constructs an ImageDecoderImpl with a given + * SeekableStream and ImageDecodeParam + * instance. + */ + public ImageDecoderImpl(SeekableStream input, + ImageDecodeParam param) { + this.input = input; + this.param = param; + } + + /** + * Constructs an ImageDecoderImpl with a given + * InputStream and ImageDecodeParam + * instance. The input parameter will be used to + * construct a ForwardSeekableStream; if the ability + * to seek backwards is required, the caller should construct + * an instance of SeekableStream and + * make use of the other contructor. + */ + public ImageDecoderImpl(InputStream input, + ImageDecodeParam param) { + this.input = new ForwardSeekableStream(input); + this.param = param; + } + + /** + * Returns the current parameters as an instance of the + * ImageDecodeParam interface. Concrete + * implementations of this interface will return corresponding + * concrete implementations of the ImageDecodeParam + * interface. For example, a JPEGImageDecoder will + * return an instance of JPEGDecodeParam. + */ + public ImageDecodeParam getParam() { + return param; + } + + /** + * Sets the current parameters to an instance of the + * ImageDecodeParam interface. Concrete + * implementations of ImageDecoder may throw a + * RuntimeException if the param + * argument is not an instance of the appropriate subclass or + * subinterface. For example, a JPEGImageDecoder + * will expect param to be an instance of + * JPEGDecodeParam. + */ + public void setParam(ImageDecodeParam param) { + this.param = param; + } + + /** + * Returns the SeekableStream associated with + * this ImageDecoder. + */ + public SeekableStream getInputStream() { + return input; + } + + /** + * Returns the number of pages present in the current stream. + * By default, the return value is 1. Subclasses that deal with + * multi-page formats should override this method. + */ + public int getNumPages() throws IOException { + return 1; + } + + /** + * Returns a Raster that contains the decoded + * contents of the SeekableStream associated + * with this ImageDecoder. Only + * the first page of a multi-page image is decoded. + */ + public Raster decodeAsRaster() throws IOException { + return decodeAsRaster(0); + } + + /** + * Returns a Raster that contains the decoded + * contents of the SeekableStream associated + * with this ImageDecoder. + * The given page of a multi-page image is decoded. If + * the page does not exist, an IOException will be thrown. + * Page numbering begins at zero. + * + * @param page The page to be decoded. + */ + public Raster decodeAsRaster(int page) throws IOException { + RenderedImage im = decodeAsRenderedImage(page); + return im.getData(); + } + + /** + * Returns a RenderedImage that contains the decoded + * contents of the SeekableStream associated + * with this ImageDecoder. Only + * the first page of a multi-page image is decoded. + */ + public RenderedImage decodeAsRenderedImage() throws IOException { + return decodeAsRenderedImage(0); + } + + /** + * Returns a RenderedImage that contains the decoded + * contents of the SeekableStream associated + * with this ImageDecoder. + * The given page of a multi-page image is decoded. If + * the page does not exist, an IOException will be thrown. + * Page numbering begins at zero. + * + * @param page The page to be decoded. + */ + public abstract RenderedImage decodeAsRenderedImage(int page) + throws IOException; +} diff --git a/src/main/java/org/apache/xmlgraphics/image/codec/util/ImageEncodeParam.java b/src/main/java/org/apache/xmlgraphics/image/codec/util/ImageEncodeParam.java new file mode 100644 index 0000000..bd8fabd --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/codec/util/ImageEncodeParam.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageEncodeParam.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.image.codec.util; + +import java.io.Serializable; + +/** + * An empty (marker) interface to be implemented by all image encoder + * parameter classes. + * + *

    This interface is not a committed part of the JAI API. It may + * be removed or changed in future releases of JAI. + */ +public interface ImageEncodeParam extends + ImageDecodeParam, Cloneable, Serializable { +} diff --git a/src/main/java/org/apache/xmlgraphics/image/codec/util/ImageEncoder.java b/src/main/java/org/apache/xmlgraphics/image/codec/util/ImageEncoder.java new file mode 100644 index 0000000..1d30b0e --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/codec/util/ImageEncoder.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageEncoder.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.image.codec.util; + +import java.awt.image.ColorModel; +import java.awt.image.Raster; +import java.awt.image.RenderedImage; +import java.io.IOException; +import java.io.OutputStream; + +/** + * An interface describing objects that transform a BufferedImage or + * Raster into an OutputStream. + * + *

    This interface is not a committed part of the JAI API. It may + * be removed or changed in future releases of JAI. + */ +public interface ImageEncoder { + + /** + * Returns the current parameters as an instance of the + * ImageEncodeParam interface. Concrete implementations of this + * interface will return corresponding concrete implementations of + * the ImageEncodeParam interface. For example, a JPEGImageEncoder + * will return an instance of JPEGEncodeParam. + */ + ImageEncodeParam getParam(); + + /** + * Sets the current parameters to an instance of the + * ImageEncodeParam interface. Concrete implementations + * of ImageEncoder may throw a RuntimeException if the + * params argument is not an instance of the appropriate + * subclass or subinterface. For example, a JPEGImageEncoder + * will expect param to be an instance of JPEGEncodeParam. + */ + void setParam(ImageEncodeParam param); + + /** Returns the OutputStream associated with this ImageEncoder. */ + OutputStream getOutputStream(); + + /** + * Encodes a Raster with a given ColorModel and writes the output + * to the OutputStream associated with this ImageEncoder. + */ + void encode(Raster ras, ColorModel cm) throws IOException; + + /** + * Encodes a RenderedImage and writes the output to the + * OutputStream associated with this ImageEncoder. + */ + void encode(RenderedImage im) throws IOException; +} diff --git a/src/main/java/org/apache/xmlgraphics/image/codec/util/ImageEncoderImpl.java b/src/main/java/org/apache/xmlgraphics/image/codec/util/ImageEncoderImpl.java new file mode 100644 index 0000000..b906ffd --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/codec/util/ImageEncoderImpl.java @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageEncoderImpl.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.image.codec.util; + +import java.awt.image.ColorModel; +import java.awt.image.Raster; +import java.awt.image.RenderedImage; +import java.io.IOException; +import java.io.OutputStream; + +/** + * A partial implementation of the ImageEncoder interface useful for + * subclassing. + * + *

    This class is not a committed part of the JAI API. It may + * be removed or changed in future releases of JAI. + */ +public abstract class ImageEncoderImpl implements ImageEncoder { + + /** The OutputStream associcted with this ImageEncoder. */ + protected OutputStream output; + + /** The ImageEncodeParam object associcted with this ImageEncoder. */ + protected ImageEncodeParam param; + + /** + * Constructs an ImageEncoderImpl with a given OutputStream + * and ImageEncoderParam instance. + */ + public ImageEncoderImpl(OutputStream output, + ImageEncodeParam param) { + this.output = output; + this.param = param; + } + + /** + * Returns the current parameters as an instance of the + * ImageEncodeParam interface. Concrete implementations of this + * interface will return corresponding concrete implementations of + * the ImageEncodeParam interface. For example, a JPEGImageEncoder + * will return an instance of JPEGEncodeParam. + */ + public ImageEncodeParam getParam() { + return param; + } + + /** + * Sets the current parameters to an instance of the + * ImageEncodeParam interface. Concrete implementations + * of ImageEncoder may throw a RuntimeException if the + * params argument is not an instance of the appropriate + * subclass or subinterface. For example, a JPEGImageEncoder + * will expect param to be an instance of JPEGEncodeParam. + */ + public void setParam(ImageEncodeParam param) { + this.param = param; + } + + /** Returns the OutputStream associated with this ImageEncoder. */ + public OutputStream getOutputStream() { + return output; + } + + /** + * Encodes a Raster with a given ColorModel and writes the output + * to the OutputStream associated with this ImageEncoder. + */ + public void encode(Raster ras, ColorModel cm) throws IOException { + RenderedImage im = new SingleTileRenderedImage(ras, cm); + encode(im); + } + + /** + * Encodes a RenderedImage and writes the output to the + * OutputStream associated with this ImageEncoder. + */ + public abstract void encode(RenderedImage im) throws IOException; +} diff --git a/src/main/java/org/apache/xmlgraphics/image/codec/util/ImageInputStreamSeekableStreamAdapter.java b/src/main/java/org/apache/xmlgraphics/image/codec/util/ImageInputStreamSeekableStreamAdapter.java new file mode 100644 index 0000000..b41e470 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/codec/util/ImageInputStreamSeekableStreamAdapter.java @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageInputStreamSeekableStreamAdapter.java 1683736 2015-06-05 12:18:54Z ssteiner $ */ + +package org.apache.xmlgraphics.image.codec.util; + +import java.io.IOException; + +import javax.imageio.stream.ImageInputStream; + +/** + * A subclass of SeekableStream that may be used to wrap + * a regular ImageInputStream. + */ +public class ImageInputStreamSeekableStreamAdapter extends SeekableStream { + + /** The source stream. */ + private ImageInputStream stream; + + /** + * Constructs a SeekableStream that takes + * its source data from a regular ImageInputStream. + * @param stream the underlying ImageInputStream to use + */ + public ImageInputStreamSeekableStreamAdapter(ImageInputStream stream) + throws IOException { + this.stream = stream; + } + + /** {@inheritDoc} */ + public boolean canSeekBackwards() { + return true; + } + + /** {@inheritDoc} */ + public long getFilePointer() throws IOException { + return stream.getStreamPosition(); + } + + /** {@inheritDoc} */ + public void seek(long pos) throws IOException { + stream.seek(pos); + } + + /** {@inheritDoc} */ + public int read() throws IOException { + return stream.read(); + } + + /** {@inheritDoc} */ + public int read(byte[] b, int off, int len) throws IOException { + return stream.read(b, off, len); + } + + /** {@inheritDoc} */ + public void close() throws IOException { + super.close(); + stream.close(); + } +} diff --git a/src/main/java/org/apache/xmlgraphics/image/codec/util/MemoryCacheSeekableStream.java b/src/main/java/org/apache/xmlgraphics/image/codec/util/MemoryCacheSeekableStream.java new file mode 100644 index 0000000..b2e212f --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/codec/util/MemoryCacheSeekableStream.java @@ -0,0 +1,255 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: MemoryCacheSeekableStream.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.image.codec.util; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +/** + * A subclass of SeekableStream that may be used to wrap + * a regular InputStream. Seeking backwards is supported + * by means of an in-memory cache. For greater efficiency, + * FileCacheSeekableStream should be used in + * circumstances that allow the creation of a temporary file. + * + *

    The mark() and reset() methods are + * supported. + * + *

    This class is not a committed part of the JAI API. It may + * be removed or changed in future releases of JAI. + */ +public final class MemoryCacheSeekableStream extends SeekableStream { + + /** The source input stream. */ + private InputStream src; + + /** Position of first unread byte. */ + private long pointer; + + /** Log_2 of the sector size. */ + private static final int SECTOR_SHIFT = 9; + + /** The sector size. */ + private static final int SECTOR_SIZE = 1 << SECTOR_SHIFT; + + /** A mask to determine the offset within a sector. */ + private static final int SECTOR_MASK = SECTOR_SIZE - 1; + + /** A Vector of source sectors. */ + private List data = new ArrayList(); + + /** Number of sectors stored. */ +// int sectors = 0; + + /** Number of bytes read. */ + int length; + + /** True if we've previously reached the end of the source stream */ + boolean foundEOS; + + /** + * Constructs a MemoryCacheSeekableStream that takes + * its source data from a regular InputStream. + * Seeking backwards is supported by means of an in-memory cache. + */ + public MemoryCacheSeekableStream(InputStream src) { + this.src = src; + } + + /** + * Ensures that at least pos bytes are cached, + * or the end of the source is reached. The return value + * is equal to the smaller of pos and the + * length of the source stream. + */ + private long readUntil(long pos) throws IOException { + // We've already got enough data cached + if (pos < length) { + return pos; + } + // pos >= length but length isn't getting any bigger, so return it + if (foundEOS) { + return length; + } + + int sector = (int)(pos >> SECTOR_SHIFT); + + // First unread sector + int startSector = length >> SECTOR_SHIFT; + + // Read sectors until the desired sector + for (int i = startSector; i <= sector; i++) { + byte[] buf = new byte[SECTOR_SIZE]; + data.add(buf); + + // Read up to SECTOR_SIZE bytes + int len = SECTOR_SIZE; + int off = 0; + while (len > 0) { + int nbytes = src.read(buf, off, len); + // Found the end-of-stream + if (nbytes == -1) { + foundEOS = true; + return length; + } + off += nbytes; + len -= nbytes; + + // Record new data length + length += nbytes; + } + } + + return length; + } + + /** + * Returns true since all + * MemoryCacheSeekableStream instances support seeking + * backwards. + */ + public boolean canSeekBackwards() { + return true; + } + + /** + * Returns the current offset in this file. + * + * @return the offset from the beginning of the file, in bytes, + * at which the next read occurs. + */ + public long getFilePointer() { + return pointer; + } + + /** + * Sets the file-pointer offset, measured from the beginning of this + * file, at which the next read occurs. + * + * @param pos the offset position, measured in bytes from the + * beginning of the file, at which to set the file + * pointer. + * @exception IOException if pos is less than + * 0 or if an I/O error occurs. + */ + public void seek(long pos) throws IOException { + if (pos < 0) { + throw new IOException(PropertyUtil.getString("MemoryCacheSeekableStream0")); + } + pointer = pos; + } + + /** + * Reads the next byte of data from the input stream. The value byte is + * returned as an int in the range 0 to + * 255. If no byte is available because the end of the stream + * has been reached, the value -1 is returned. This method + * blocks until input data is available, the end of the stream is detected, + * or an exception is thrown. + * + * @return the next byte of data, or -1 if the end of the + * stream is reached. + */ + public int read() throws IOException { + long next = pointer + 1; + long pos = readUntil(next); + if (pos >= next) { + byte[] buf = + (byte[])data.get((int)(pointer >> SECTOR_SHIFT)); + return buf[(int)(pointer++ & SECTOR_MASK)] & 0xff; + } else { + return -1; + } + } + + /** + * Reads up to len bytes of data from the input stream into + * an array of bytes. An attempt is made to read as many as + * len bytes, but a smaller number may be read, possibly + * zero. The number of bytes actually read is returned as an integer. + * + *

    This method blocks until input data is available, end of file is + * detected, or an exception is thrown. + * + *

    If b is null, a + * NullPointerException is thrown. + * + *

    If off is negative, or len is negative, or + * off+len is greater than the length of the array + * b, then an IndexOutOfBoundsException is + * thrown. + * + *

    If len is zero, then no bytes are read and + * 0 is returned; otherwise, there is an attempt to read at + * least one byte. If no byte is available because the stream is at end of + * file, the value -1 is returned; otherwise, at least one + * byte is read and stored into b. + * + *

    The first byte read is stored into element b[off], the + * next one into b[off+1], and so on. The number of bytes read + * is, at most, equal to len. Let k be the number of + * bytes actually read; these bytes will be stored in elements + * b[off] through b[off+k-1], + * leaving elements b[off+k] through + * b[off+len-1] unaffected. + * + *

    In every case, elements b[0] through + * b[off] and elements b[off+len] through + * b[b.length-1] are unaffected. + * + *

    If the first byte cannot be read for any reason other than end of + * file, then an IOException is thrown. In particular, an + * IOException is thrown if the input stream has been closed. + * + * @param b the buffer into which the data is read. + * @param off the start offset in array b + * at which the data is written. + * @param len the maximum number of bytes to read. + * @return the total number of bytes read into the buffer, or + * -1 if there is no more data because the end of + * the stream has been reached. + */ + public int read(byte[] b, int off, int len) throws IOException { + if (b == null) { + throw new NullPointerException(); + } + if ((off < 0) || (len < 0) || (off + len > b.length)) { + throw new IndexOutOfBoundsException(); + } + if (len == 0) { + return 0; + } + + long pos = readUntil(pointer + len); + // End-of-stream + if (pos <= pointer) { + return -1; + } + + byte[] buf = (byte[])data.get((int)(pointer >> SECTOR_SHIFT)); + int nbytes = Math.min(len, SECTOR_SIZE - (int)(pointer & SECTOR_MASK)); + System.arraycopy(buf, (int)(pointer & SECTOR_MASK), + b, off, nbytes); + pointer += nbytes; + return nbytes; + } +} diff --git a/src/main/java/org/apache/xmlgraphics/image/codec/util/PropertyUtil.java b/src/main/java/org/apache/xmlgraphics/image/codec/util/PropertyUtil.java new file mode 100644 index 0000000..2386d8d --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/codec/util/PropertyUtil.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PropertyUtil.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.image.codec.util; + +import java.util.MissingResourceException; + +import org.apache.xmlgraphics.util.i18n.LocalizableSupport; + +public final class PropertyUtil { + + private PropertyUtil() { + } + + private static final String RESOURCES = + "org.apache.xmlgraphics.image.codec.Messages"; + + + private static final LocalizableSupport LOCALIZABLESUPPORT = + new LocalizableSupport( + RESOURCES, PropertyUtil.class.getClassLoader()); + + public static String getString(String key) { + try { + return LOCALIZABLESUPPORT.formatMessage(key, null); + } catch (MissingResourceException e) { + return key; + } + } +} diff --git a/src/main/java/org/apache/xmlgraphics/image/codec/util/SeekableOutputStream.java b/src/main/java/org/apache/xmlgraphics/image/codec/util/SeekableOutputStream.java new file mode 100644 index 0000000..77cbd1d --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/codec/util/SeekableOutputStream.java @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: SeekableOutputStream.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.image.codec.util; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.RandomAccessFile; + +/** + * An OutputStream which can seek to an arbitrary offset. + * + * @version $Id: SeekableOutputStream.java 1732018 2016-02-24 04:51:06Z gadams $ + */ +public class SeekableOutputStream extends OutputStream { + + private RandomAccessFile file; + + /** + * Constructs a SeekableOutputStream from a + * RandomAccessFile. Unless otherwise indicated, + * all method invocations are fowarded to the underlying + * RandomAccessFile. + * + * @param file The RandomAccessFile to which calls + * will be forwarded. + * @exception IllegalArgumentException if file is + * null. + */ + public SeekableOutputStream(RandomAccessFile file) { + if (file == null) { + throw new IllegalArgumentException("SeekableOutputStream0"); + } + this.file = file; + } + + public void write(int b) throws IOException { + file.write(b); + } + + public void write(byte[] b) throws IOException { + file.write(b); + } + + public void write(byte[] b, int off, int len) throws IOException { + file.write(b, off, len); + } + + /** + * Invokes getFD().sync() on the underlying + * RandomAccessFile. + */ + public void flush() throws IOException { + file.getFD().sync(); + } + + public void close() throws IOException { + file.close(); + } + + public long getFilePointer() throws IOException { + return file.getFilePointer(); + } + + public void seek(long pos) throws IOException { + file.seek(pos); + } +} diff --git a/src/main/java/org/apache/xmlgraphics/image/codec/util/SeekableStream.java b/src/main/java/org/apache/xmlgraphics/image/codec/util/SeekableStream.java new file mode 100644 index 0000000..240ca9e --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/codec/util/SeekableStream.java @@ -0,0 +1,943 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: SeekableStream.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.image.codec.util; + +import java.io.DataInput; +import java.io.DataInputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; + +/** + * An abstract subclass of java.io.InputStream that allows seeking + * within the input, similar to the RandomAccessFile class. + * Additionally, the DataInput interface is supported and extended + * to include support for little-endian representations of fundamental data + * types. + * + *

    In addition to the familiar methods from InputStream, the + * methods getFilePointer(), seek(), are defined as in + * the RandomAccessFile class. The canSeekBackwards() + * method will return true if it is permissible to seek to a + * position earlier in the stream than the current value of + * getFilePointer(). Some subclasses of + * SeekableStream guarantee the ability to seek backwards while + * others may not offer this feature in the interest of providing greater + * efficiency for those users who do not require it. + * + *

    The DataInput interface is supported as well. This included + * the skipBytes() and readFully() methods and a + * variety of read methods for various data types. + * + *

    Three classes are provided for the purpose of adapting a standard + * InputStream to the SeekableStream interface. + * ForwardSeekableStream does not allows seeking backwards, but is + * inexpensive to use. FileCacheSeekableStream maintains a copy of + * all of the data read from the input in a temporary file; this file will be + * discarded automatically when the FileSeekableStream is + * finalized, or when the JVM exits normally. + * FileCacheSeekableStream is intended to be reasonably efficient + * apart from the unavoidable use of disk space. In circumstances where the + * creation of a temporary file is not possible, + * MemoryCacheSeekableStream may be used. + * MemoryCacheSeekableStream creates a potentially large in-memory + * buffer to store the stream data and so should be avoided when possible. + * + *

    The FileSeekableStream class wraps a File or + * RandomAccessFile. It forwards requests to the real underlying + * file. It performs a limited amount of caching in order to avoid excessive + * I/O costs. + * + *

    The SegmentedSeekableStream class performs a different sort + * of function. It creates a SeekableStream from another + * SeekableStream by selecting a series of portions or "segments". + * Each segment starts at a specified location within the source + * SeekableStream and extends for a specified number of bytes. The + * StreamSegmentMapper interface and StreamSegment + * class may be used to compute the segment positions dynamically. + * + *

    A convenience methods, wrapInputStream is provided to + * construct a suitable SeekableStream instance whose data is + * supplied by a given InputStream. The caller, by means of the + * canSeekBackwards parameter, determines whether support for + * seeking backwards is required. + * + */ +public abstract class SeekableStream extends InputStream implements DataInput { + + /** + * Returns a SeekableStream that will read from a + * given InputStream, optionally including support + * for seeking backwards. This is a convenience method that + * avoids the need to instantiate specific subclasses of + * SeekableStream depending on the current security + * model. + * + * @param is An InputStream. + * @param canSeekBackwards true if the ability to seek + * backwards in the output is required. + * @return An instance of SeekableStream. + */ + public static SeekableStream wrapInputStream(InputStream is, + boolean canSeekBackwards) { + SeekableStream stream = null; + + if (canSeekBackwards) { + try { + stream = new FileCacheSeekableStream(is); + } catch (Exception e) { + stream = new MemoryCacheSeekableStream(is); + } + } else { + stream = new ForwardSeekableStream(is); + } + return stream; + } + + // Methods from InputStream + + /** + * Reads the next byte of data from the input stream. The value byte is + * returned as an int in the range 0 to + * 255. If no byte is available because the end of the stream + * has been reached, the value -1 is returned. This method + * blocks until input data is available, the end of the stream is detected, + * or an exception is thrown. + * + *

    A subclass must provide an implementation of this method. + * + * @return the next byte of data, or -1 if the end of the + * stream is reached. + * @exception IOException if an I/O error occurs. + */ + public abstract int read() throws IOException; + + /** + * Reads up to len bytes of data from the input stream into + * an array of bytes. An attempt is made to read as many as + * len bytes, but a smaller number may be read, possibly + * zero. The number of bytes actually read is returned as an integer. + * + *

    This method blocks until input data is available, end of stream is + * detected, or an exception is thrown. + * + *

    If b is null, a + * NullPointerException is thrown. + * + *

    If off is negative, or len is negative, or + * off+len is greater than the length of the array + * b, then an IndexOutOfBoundsException is + * thrown. + * + *

    If len is zero, then no bytes are read and + * 0 is returned; otherwise, there is an attempt to read at + * least one byte. If no byte is available because the stream is at end of + * stream, the value -1 is returned; otherwise, at least one + * byte is read and stored into b. + * + *

    The first byte read is stored into element b[off], the + * next one into b[off+1], and so on. The number of bytes read + * is, at most, equal to len. Let k be the number of + * bytes actually read; these bytes will be stored in elements + * b[off] through b[off+k-1], + * leaving elements b[off+k] through + * b[off+len-1] unaffected. + * + *

    In every case, elements b[0] through + * b[off] and elements b[off+len] through + * b[b.length-1] are unaffected. + * + *

    If the first byte cannot be read for any reason other than end of + * stream, then an IOException is thrown. In particular, an + * IOException is thrown if the input stream has been closed. + * + *

    A subclass must provide an implementation of this method. + * + * @param b the buffer into which the data is read. + * @param off the start offset in array b + * at which the data is written. + * @param len the maximum number of bytes to read. + * @return the total number of bytes read into the buffer, or + * -1 if there is no more data because the end of + * the stream has been reached. + * @exception IOException if an I/O error occurs. + */ + public abstract int read(byte[] b, int off, int len) throws IOException; + + // Implemented in InputStream: + // + // public int read(byte[] b) throws IOException { + // public long skip(long n) throws IOException + // public int available) throws IOException + // public void close() throws IOException; + + /** Marked position, shared by {@link ForwardSeekableStream} */ + protected long markPos = -1L; + + /** + * Marks the current file position for later return using + * the reset() method. + */ + public synchronized void mark(int readLimit) { + try { + markPos = getFilePointer(); + } catch (IOException e) { + markPos = -1L; + } + } + + /** + * Returns the file position to its position at the time of + * the immediately previous call to the mark() + * method. + */ + public synchronized void reset() throws IOException { + if (markPos != -1) { + seek(markPos); + } + } + + /** + * Returns true if marking is supported. + * Marking is automatically supported for SeekableStream + * subclasses that support seeking backeards. Subclasses that do + * not support seeking backwards but do support marking must override + * this method. + */ + public boolean markSupported() { + return canSeekBackwards(); + } + + /** + * Returns true if this object supports calls to + * seek(pos) with an offset pos smaller + * than the current offset, as returned by getFilePointer. + */ + public boolean canSeekBackwards() { + return false; + } + + /** + * Returns the current offset in this stream. + * + * @return the offset from the beginning of the stream, in bytes, + * at which the next read occurs. + * @exception IOException if an I/O error occurs. + */ + public abstract long getFilePointer() throws IOException; + + /** + * Sets the offset, measured from the beginning of this + * stream, at which the next read occurs. + * + *

    If canSeekBackwards() returns false, + * then setting pos to an offset smaller than + * the current value of getFilePointer() will have + * no effect. + * + * @param pos the offset position, measured in bytes from the + * beginning of the stream, at which to set the stream + * pointer. + * @exception IOException if pos is less than + * 0 or if an I/O error occurs. + */ + public abstract void seek(long pos) throws IOException; + + // Methods from RandomAccessFile + + /** + * Reads b.length bytes from this stream into the byte + * array, starting at the current stream pointer. This method reads + * repeatedly from the stream until the requested number of bytes are + * read. This method blocks until the requested number of bytes are + * read, the end of the stream is detected, or an exception is thrown. + * + * @param b the buffer into which the data is read. + * @exception EOFException if this stream reaches the end before reading + * all the bytes. + * @exception IOException if an I/O error occurs. + */ + public final void readFully(byte[] b) throws IOException { + readFully(b, 0, b.length); + } + + /** + * Reads exactly len bytes from this stream into the byte + * array, starting at the current stream pointer. This method reads + * repeatedly from the stream until the requested number of bytes are + * read. This method blocks until the requested number of bytes are + * read, the end of the stream is detected, or an exception is thrown. + * + * @param b the buffer into which the data is read. + * @param off the start offset of the data. + * @param len the number of bytes to read. + * @exception EOFException if this stream reaches the end before reading + * all the bytes. + * @exception IOException if an I/O error occurs. + */ + public final void readFully(byte[] b, int off, int len) + throws IOException { + int n = 0; + do { + int count = this.read(b, off + n, len - n); + if (count < 0) { + throw new EOFException(); + } + n += count; + } while (n < len); + } + + // Methods from DataInput, plus little-endian versions + + /** + * Attempts to skip over n bytes of input discarding the + * skipped bytes. + *

    + * + * This method may skip over some smaller number of bytes, possibly zero. + * This may result from any of a number of conditions; reaching end of + * stream before n bytes have been skipped is only one + * possibility. This method never throws an EOFException. + * The actual number of bytes skipped is returned. If n + * is negative, no bytes are skipped. + * + * @param n the number of bytes to be skipped. + * @return the actual number of bytes skipped. + * @exception IOException if an I/O error occurs. + */ + public int skipBytes(int n) throws IOException { + if (n <= 0) { + return 0; + } + return (int)skip(n); + } + + /** + * Reads a boolean from this stream. This method reads a + * single byte from the stream, starting at the current stream pointer. + * A value of 0 represents + * false. Any other value represents true. + * This method blocks until the byte is read, the end of the stream + * is detected, or an exception is thrown. + * + * @return the boolean value read. + * @exception EOFException if this stream has reached the end. + * @exception IOException if an I/O error occurs. + */ + public final boolean readBoolean() throws IOException { + int ch = this.read(); + if (ch < 0) { + throw new EOFException(); + } + return (ch != 0); + } + + /** + * Reads a signed eight-bit value from this stream. This method reads a + * byte from the stream, starting from the current stream pointer. + * If the byte read is b, where + * 0 <= b <= 255, + * then the result is: + *

    +     *     (byte)(b)
    +     * 
    + *

    + * This method blocks until the byte is read, the end of the stream + * is detected, or an exception is thrown. + * + * @return the next byte of this stream as a signed eight-bit + * byte. + * @exception EOFException if this stream has reached the end. + * @exception IOException if an I/O error occurs. + */ + public final byte readByte() throws IOException { + int ch = this.read(); + if (ch < 0) { + throw new EOFException(); + } + return (byte)(ch); + } + + /** + * Reads an unsigned eight-bit number from this stream. This method reads + * a byte from this stream, starting at the current stream pointer, + * and returns that byte. + *

    + * This method blocks until the byte is read, the end of the stream + * is detected, or an exception is thrown. + * + * @return the next byte of this stream, interpreted as an unsigned + * eight-bit number. + * @exception EOFException if this stream has reached the end. + * @exception IOException if an I/O error occurs. + */ + public final int readUnsignedByte() throws IOException { + int ch = this.read(); + if (ch < 0) { + throw new EOFException(); + } + return ch; + } + + /** + * Reads a signed 16-bit number from this stream. + * The method reads two + * bytes from this stream, starting at the current stream pointer. + * If the two bytes read, in order, are + * b1 and b2, where each of the two values is + * between 0 and 255, inclusive, then the + * result is equal to: + *

    +     *     (short)((b1 << 8) | b2)
    +     * 
    + *

    + * This method blocks until the two bytes are read, the end of the + * stream is detected, or an exception is thrown. + * + * @return the next two bytes of this stream, interpreted as a signed + * 16-bit number. + * @exception EOFException if this stream reaches the end before reading + * two bytes. + * @exception IOException if an I/O error occurs. + */ + public final short readShort() throws IOException { + int ch1 = this.read(); + int ch2 = this.read(); + if ((ch1 | ch2) < 0) { + throw new EOFException(); + } + return (short)((ch1 << 8) + (ch2 << 0)); + } + + /** + * Reads a signed 16-bit number from this stream in little-endian order. + * The method reads two + * bytes from this stream, starting at the current stream pointer. + * If the two bytes read, in order, are + * b1 and b2, where each of the two values is + * between 0 and 255, inclusive, then the + * result is equal to: + *

    +     *     (short)((b2 << 8) | b1)
    +     * 
    + *

    + * This method blocks until the two bytes are read, the end of the + * stream is detected, or an exception is thrown. + * + * @return the next two bytes of this stream, interpreted as a signed + * 16-bit number. + * @exception EOFException if this stream reaches the end before reading + * two bytes. + * @exception IOException if an I/O error occurs. + */ + public final short readShortLE() throws IOException { + int ch1 = this.read(); + int ch2 = this.read(); + if ((ch1 | ch2) < 0) { + throw new EOFException(); + } + return (short)((ch2 << 8) + (ch1 << 0)); + } + + /** + * Reads an unsigned 16-bit number from this stream. This method reads + * two bytes from the stream, starting at the current stream pointer. + * If the bytes read, in order, are + * b1 and b2, where + * 0 <= b1, b2 <= 255, + * then the result is equal to: + *

    +     *     (b1 << 8) | b2
    +     * 
    + *

    + * This method blocks until the two bytes are read, the end of the + * stream is detected, or an exception is thrown. + * + * @return the next two bytes of this stream, interpreted as an + * unsigned 16-bit integer. + * @exception EOFException if this stream reaches the end before reading + * two bytes. + * @exception IOException if an I/O error occurs. + */ + public final int readUnsignedShort() throws IOException { + int ch1 = this.read(); + int ch2 = this.read(); + if ((ch1 | ch2) < 0) { + throw new EOFException(); + } + return (ch1 << 8) + (ch2 << 0); + } + + /** + * Reads an unsigned 16-bit number from this stream in little-endian order. + * This method reads + * two bytes from the stream, starting at the current stream pointer. + * If the bytes read, in order, are + * b1 and b2, where + * 0 <= b1, b2 <= 255, + * then the result is equal to: + *

    +     *     (b2 << 8) | b1
    +     * 
    + *

    + * This method blocks until the two bytes are read, the end of the + * stream is detected, or an exception is thrown. + * + * @return the next two bytes of this stream, interpreted as an + * unsigned 16-bit integer. + * @exception EOFException if this stream reaches the end before reading + * two bytes. + * @exception IOException if an I/O error occurs. + */ + public final int readUnsignedShortLE() throws IOException { + int ch1 = this.read(); + int ch2 = this.read(); + if ((ch1 | ch2) < 0) { + throw new EOFException(); + } + return (ch2 << 8) + (ch1 << 0); + } + + /** + * Reads a Unicode character from this stream. This method reads two + * bytes from the stream, starting at the current stream pointer. + * If the bytes read, in order, are + * b1 and b2, where + * 0 <= b1, b2 <= 255, + * then the result is equal to: + *

    +     *     (char)((b1 << 8) | b2)
    +     * 
    + *

    + * This method blocks until the two bytes are read, the end of the + * stream is detected, or an exception is thrown. + * + * @return the next two bytes of this stream as a Unicode character. + * @exception EOFException if this stream reaches the end before reading + * two bytes. + * @exception IOException if an I/O error occurs. + */ + public final char readChar() throws IOException { + int ch1 = this.read(); + int ch2 = this.read(); + if ((ch1 | ch2) < 0) { + throw new EOFException(); + } + return (char)((ch1 << 8) + (ch2 << 0)); + } + + /** + * Reads a Unicode character from this stream in little-endian order. + * This method reads two + * bytes from the stream, starting at the current stream pointer. + * If the bytes read, in order, are + * b1 and b2, where + * 0 <= b1, b2 <= 255, + * then the result is equal to: + *

    +     *     (char)((b2 << 8) | b1)
    +     * 
    + *

    + * This method blocks until the two bytes are read, the end of the + * stream is detected, or an exception is thrown. + * + * @return the next two bytes of this stream as a Unicode character. + * @exception EOFException if this stream reaches the end before reading + * two bytes. + * @exception IOException if an I/O error occurs. + */ + public final char readCharLE() throws IOException { + int ch1 = this.read(); + int ch2 = this.read(); + if ((ch1 | ch2) < 0) { + throw new EOFException(); + } + return (char)((ch2 << 8) + (ch1 << 0)); + } + + /** + * Reads a signed 32-bit integer from this stream. This method reads 4 + * bytes from the stream, starting at the current stream pointer. + * If the bytes read, in order, are b1, + * b2, b3, and b4, where + * 0 <= b1, b2, b3, b4 <= 255, + * then the result is equal to: + *

    +     *     (b1 << 24) | (b2 << 16) + (b3 << 8) + b4
    +     * 
    + *

    + * This method blocks until the four bytes are read, the end of the + * stream is detected, or an exception is thrown. + * + * @return the next four bytes of this stream, interpreted as an + * int. + * @exception EOFException if this stream reaches the end before reading + * four bytes. + * @exception IOException if an I/O error occurs. + */ + public final int readInt() throws IOException { + int ch1 = this.read(); + int ch2 = this.read(); + int ch3 = this.read(); + int ch4 = this.read(); + if ((ch1 | ch2 | ch3 | ch4) < 0) { + throw new EOFException(); + } + return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + (ch4 << 0)); + } + + /** + * Reads a signed 32-bit integer from this stream in little-endian order. + * This method reads 4 + * bytes from the stream, starting at the current stream pointer. + * If the bytes read, in order, are b1, + * b2, b3, and b4, where + * 0 <= b1, b2, b3, b4 <= 255, + * then the result is equal to: + *

    +     *     (b4 << 24) | (b3 << 16) + (b2 << 8) + b1
    +     * 
    + *

    + * This method blocks until the four bytes are read, the end of the + * stream is detected, or an exception is thrown. + * + * @return the next four bytes of this stream, interpreted as an + * int. + * @exception EOFException if this stream reaches the end before reading + * four bytes. + * @exception IOException if an I/O error occurs. + */ + public final int readIntLE() throws IOException { + int ch1 = this.read(); + int ch2 = this.read(); + int ch3 = this.read(); + int ch4 = this.read(); + if ((ch1 | ch2 | ch3 | ch4) < 0) { + throw new EOFException(); + } + return ((ch4 << 24) + (ch3 << 16) + (ch2 << 8) + (ch1 << 0)); + } + + /** + * Reads an unsigned 32-bit integer from this stream. This method reads 4 + * bytes from the stream, starting at the current stream pointer. + * If the bytes read, in order, are b1, + * b2, b3, and b4, where + * 0 <= b1, b2, b3, b4 <= 255, + * then the result is equal to: + *

    +     *     (b1 << 24) | (b2 << 16) + (b3 << 8) + b4
    +     * 
    + *

    + * This method blocks until the four bytes are read, the end of the + * stream is detected, or an exception is thrown. + * + * @return the next four bytes of this stream, interpreted as a + * long. + * @exception EOFException if this stream reaches the end before reading + * four bytes. + * @exception IOException if an I/O error occurs. + */ + public final long readUnsignedInt() throws IOException { + long ch1 = this.read(); + long ch2 = this.read(); + long ch3 = this.read(); + long ch4 = this.read(); + if ((ch1 | ch2 | ch3 | ch4) < 0) { + throw new EOFException(); + } + return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + (ch4 << 0)); + } + + private byte[] ruileBuf = new byte[4]; + + /** + * Reads an unsigned 32-bit integer from this stream in little-endian + * order. This method reads 4 + * bytes from the stream, starting at the current stream pointer. + * If the bytes read, in order, are b1, + * b2, b3, and b4, where + * 0 <= b1, b2, b3, b4 <= 255, + * then the result is equal to: + *

    +     *     (b4 << 24) | (b3 << 16) + (b2 << 8) + b1
    +     * 
    + *

    + * This method blocks until the four bytes are read, the end of the + * stream is detected, or an exception is thrown. + * + * @return the next four bytes of this stream, interpreted as a + * long. + * @exception EOFException if this stream reaches the end before reading + * four bytes. + * @exception IOException if an I/O error occurs. + */ + public final long readUnsignedIntLE() throws IOException { + this.readFully(ruileBuf); + long ch1 = (ruileBuf[0] & 0xff); + long ch2 = (ruileBuf[1] & 0xff); + long ch3 = (ruileBuf[2] & 0xff); + long ch4 = (ruileBuf[3] & 0xff); + + return ((ch4 << 24) + (ch3 << 16) + (ch2 << 8) + (ch1 << 0)); + } + + /** + * Reads a signed 64-bit integer from this stream. This method reads eight + * bytes from the stream, starting at the current stream pointer. + * If the bytes read, in order, are + * b1, b2, b3, + * b4, b5, b6, + * b7, and b8, where: + *

    +     *     0 <= b1, b2, b3, b4, b5, b6, b7, b8 <=255,
    +     * 
    + *

    + * then the result is equal to: + *

    +     *     ((long)b1 << 56) + ((long)b2 << 48)
    +     *     + ((long)b3 << 40) + ((long)b4 << 32)
    +     *     + ((long)b5 << 24) + ((long)b6 << 16)
    +     *     + ((long)b7 << 8) + b8
    +     * 
    + *

    + * This method blocks until the eight bytes are read, the end of the + * stream is detected, or an exception is thrown. + * + * @return the next eight bytes of this stream, interpreted as a + * long. + * @exception EOFException if this stream reaches the end before reading + * eight bytes. + * @exception IOException if an I/O error occurs. + */ + public final long readLong() throws IOException { + return ((long)(readInt()) << 32) + (readInt() & 0xFFFFFFFFL); + } + + /** + * Reads a signed 64-bit integer from this stream in little-endian + * order. This method reads eight + * bytes from the stream, starting at the current stream pointer. + * If the bytes read, in order, are + * b1, b2, b3, + * b4, b5, b6, + * b7, and b8, where: + *

    +     *     0 <= b1, b2, b3, b4, b5, b6, b7, b8 <=255,
    +     * 
    + *

    + * then the result is equal to: + *

    +     *     ((long)b1 << 56) + ((long)b2 << 48)
    +     *     + ((long)b3 << 40) + ((long)b4 << 32)
    +     *     + ((long)b5 << 24) + ((long)b6 << 16)
    +     *     + ((long)b7 << 8) + b8
    +     * 
    + *

    + * This method blocks until the eight bytes are read, the end of the + * stream is detected, or an exception is thrown. + * + * @return the next eight bytes of this stream, interpreted as a + * long. + * @exception EOFException if this stream reaches the end before reading + * eight bytes. + * @exception IOException if an I/O error occurs. + */ + public final long readLongLE() throws IOException { + int i1 = readIntLE(); + int i2 = readIntLE(); + return ((long)i2 << 32) + (i1 & 0xFFFFFFFFL); + } + + /** + * Reads a float from this stream. This method reads an + * int value, starting at the current stream pointer, + * as if by the readInt method + * and then converts that int to a float + * using the intBitsToFloat method in class + * Float. + *

    + * This method blocks until the four bytes are read, the end of the + * stream is detected, or an exception is thrown. + * + * @return the next four bytes of this stream, interpreted as a + * float. + * @exception EOFException if this stream reaches the end before reading + * four bytes. + * @exception IOException if an I/O error occurs. + */ + public final float readFloat() throws IOException { + return Float.intBitsToFloat(readInt()); + } + + /** + * Reads a float from this stream in little-endian order. + * This method reads an + * int value, starting at the current stream pointer, + * as if by the readInt method + * and then converts that int to a float + * using the intBitsToFloat method in class + * Float. + *

    + * This method blocks until the four bytes are read, the end of the + * stream is detected, or an exception is thrown. + * + * @return the next four bytes of this stream, interpreted as a + * float. + * @exception EOFException if this stream reaches the end before reading + * four bytes. + * @exception IOException if an I/O error occurs. + */ + public final float readFloatLE() throws IOException { + return Float.intBitsToFloat(readIntLE()); + } + + /** + * Reads a double from this stream. This method reads a + * long value, starting at the current stream pointer, + * as if by the readLong method + * and then converts that long to a double + * using the longBitsToDouble method in + * class Double. + *

    + * This method blocks until the eight bytes are read, the end of the + * stream is detected, or an exception is thrown. + * + * @return the next eight bytes of this stream, interpreted as a + * double. + * @exception EOFException if this stream reaches the end before reading + * eight bytes. + * @exception IOException if an I/O error occurs. + */ + public final double readDouble() throws IOException { + return Double.longBitsToDouble(readLong()); + } + + /** + * Reads a double from this stream in little-endian order. + * This method reads a + * long value, starting at the current stream pointer, + * as if by the readLong method + * and then converts that long to a double + * using the longBitsToDouble method in + * class Double. + *

    + * This method blocks until the eight bytes are read, the end of the + * stream is detected, or an exception is thrown. + * + * @return the next eight bytes of this stream, interpreted as a + * double. + * @exception EOFException if this stream reaches the end before reading + * eight bytes. + * @exception IOException if an I/O error occurs. + */ + public final double readDoubleLE() throws IOException { + return Double.longBitsToDouble(readLongLE()); + } + + /** + * Reads the next line of text from this stream. This method successively + * reads bytes from the stream, starting at the current stream pointer, + * until it reaches a line terminator or the end + * of the stream. Each byte is converted into a character by taking the + * byte's value for the lower eight bits of the character and setting the + * high eight bits of the character to zero. This method does not, + * therefore, support the full Unicode character set. + * + *

    A line of text is terminated by a carriage-return character + * ('\r'), a newline character ('\n'), a + * carriage-return character immediately followed by a newline character, + * or the end of the stream. Line-terminating characters are discarded and + * are not included as part of the string returned. + * + *

    This method blocks until a newline character is read, a carriage + * return and the byte following it are read (to see if it is a newline), + * the end of the stream is reached, or an exception is thrown. + * + * @return the next line of text from this stream, or null if end + * of stream is encountered before even one byte is read. + * @exception IOException if an I/O error occurs. + */ + public final String readLine() throws IOException { + StringBuffer input = new StringBuffer(); + int c = -1; + boolean eol = false; + + while (!eol) { + c = read(); + switch (c) { + case -1: + case '\n': + eol = true; + break; + case '\r': + eol = true; + long cur = getFilePointer(); + if ((read()) != '\n') { + seek(cur); + } + break; + default: + input.append((char)c); + break; + } + } + + if ((c == -1) && (input.length() == 0)) { + return null; + } + return input.toString(); + } + + /** + * Reads in a string from this stream. The string has been encoded + * using a modified UTF-8 format. + *

    + * The first two bytes are read, starting from the current stream + * pointer, as if by + * readUnsignedShort. This value gives the number of + * following bytes that are in the encoded string, not + * the length of the resulting string. The following bytes are then + * interpreted as bytes encoding characters in the UTF-8 format + * and are converted into characters. + *

    + * This method blocks until all the bytes are read, the end of the + * stream is detected, or an exception is thrown. + * + * @return a Unicode string. + * @exception EOFException if this stream reaches the end before + * reading all the bytes. + * @exception IOException if an I/O error occurs. + * @exception java.io.UTFDataFormatException if the bytes do not represent + * valid UTF-8 encoding of a Unicode string. + */ + public final String readUTF() throws IOException { + return DataInputStream.readUTF(this); + } + + /** + * Releases any system resources associated with this stream + * by calling the close() method. + */ + protected void finalize() throws Throwable { + super.finalize(); + close(); + } +} diff --git a/src/main/java/org/apache/xmlgraphics/image/codec/util/SimpleRenderedImage.java b/src/main/java/org/apache/xmlgraphics/image/codec/util/SimpleRenderedImage.java new file mode 100644 index 0000000..4303f03 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/codec/util/SimpleRenderedImage.java @@ -0,0 +1,532 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: SimpleRenderedImage.java 1804124 2017-08-04 14:13:54Z ssteiner $ */ + +package org.apache.xmlgraphics.image.codec.util; + +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.image.ColorModel; +import java.awt.image.Raster; +import java.awt.image.RenderedImage; +import java.awt.image.SampleModel; +import java.awt.image.WritableRaster; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Vector; + +// CSOFF: WhitespaceAround + +/** + * A simple class implemented the RenderedImage + * interface. Only the getTile() method needs to be + * implemented by subclasses. The instance variables must also be + * filled in properly. + * + *

    Normally in JAI PlanarImage is used for this + * purpose, but in the interest of modularity the + * use of PlanarImage has been avoided. + * + * @version $Id: SimpleRenderedImage.java 1804124 2017-08-04 14:13:54Z ssteiner $ + */ +public abstract class SimpleRenderedImage implements RenderedImage { + + /** The X coordinate of the image's upper-left pixel. */ + protected int minX; + + /** The Y coordinate of the image's upper-left pixel. */ + protected int minY; + + /** The image's width in pixels. */ + protected int width; + + /** The image's height in pixels. */ + protected int height; + + /** The width of a tile. */ + protected int tileWidth; + + /** The height of a tile. */ + protected int tileHeight; + + /** The X coordinate of the upper-left pixel of tile (0, 0). */ + protected int tileGridXOffset; + + /** The Y coordinate of the upper-left pixel of tile (0, 0). */ + protected int tileGridYOffset; + + /** The image's SampleModel. */ + protected SampleModel sampleModel; + + /** The image's ColorModel. */ + protected ColorModel colorModel; + + /** The image's sources, stored in a Vector. */ +// protected List sources = new ArrayList(); + + /** A Hashtable containing the image properties. */ + protected Map properties = new HashMap(); + + public SimpleRenderedImage() { } + + /** Returns the X coordinate of the leftmost column of the image. */ + public int getMinX() { + return minX; + } + + /** + * Returns the X coordinate of the column immediatetely to the + * right of the rightmost column of the image. getMaxX() is + * implemented in terms of getMinX() and getWidth() and so does + * not need to be implemented by subclasses. + */ + public final int getMaxX() { + return getMinX() + getWidth(); + } + + /** Returns the X coordinate of the uppermost row of the image. */ + public int getMinY() { + return minY; + } + + /** + * Returns the Y coordinate of the row immediately below the + * bottom row of the image. getMaxY() is implemented in terms of + * getMinY() and getHeight() and so does not need to be + * implemented by subclasses. + */ + public final int getMaxY() { + return getMinY() + getHeight(); + } + + /** Returns the width of the image. */ + public int getWidth() { + return width; + } + + /** Returns the height of the image. */ + public int getHeight() { + return height; + } + + /** Returns a Rectangle indicating the image bounds. */ + public Rectangle getBounds() { + return new Rectangle(getMinX(), getMinY(), + getWidth(), getHeight()); + } + + /** Returns the width of a tile. */ + public int getTileWidth() { + return tileWidth; + } + + /** Returns the height of a tile. */ + public int getTileHeight() { + return tileHeight; + } + + /** + * Returns the X coordinate of the upper-left pixel of tile (0, 0). + */ + public int getTileGridXOffset() { + return tileGridXOffset; + } + + /** + * Returns the Y coordinate of the upper-left pixel of tile (0, 0). + */ + public int getTileGridYOffset() { + return tileGridYOffset; + } + + /** + * Returns the horizontal index of the leftmost column of tiles. + * getMinTileX() is implemented in terms of getMinX() + * and so does not need to be implemented by subclasses. + */ + public int getMinTileX() { + return convertXToTileX(getMinX()); + } + + /** + * Returns the horizontal index of the rightmost column of tiles. + * getMaxTileX() is implemented in terms of getMaxX() + * and so does not need to be implemented by subclasses. + */ + public int getMaxTileX() { + return convertXToTileX(getMaxX() - 1); + } + + /** + * Returns the number of tiles along the tile grid in the + * horizontal direction. getNumXTiles() is implemented in terms + * of getMinTileX() and getMaxTileX() and so does not need to be + * implemented by subclasses. + */ + public int getNumXTiles() { + return getMaxTileX() - getMinTileX() + 1; + } + + /** + * Returns the vertical index of the uppermost row of tiles. getMinTileY() + * is implemented in terms of getMinY() and so does not need to be + * implemented by subclasses. + */ + public int getMinTileY() { + return convertYToTileY(getMinY()); + } + + /** + * Returns the vertical index of the bottom row of tiles. getMaxTileY() + * is implemented in terms of getMaxY() and so does not need to + * be implemented by subclasses. + */ + public int getMaxTileY() { + return convertYToTileY(getMaxY() - 1); + } + + /** + * Returns the number of tiles along the tile grid in the vertical + * direction. getNumYTiles() is implemented in terms + * of getMinTileY() and getMaxTileY() and so does not need to be + * implemented by subclasses. + */ + public int getNumYTiles() { + return getMaxTileY() - getMinTileY() + 1; + } + + /** Returns the SampleModel of the image. */ + public SampleModel getSampleModel() { + return sampleModel; + } + + /** Returns the ColorModel of the image. */ + public ColorModel getColorModel() { + return colorModel; + } + + /** + * Gets a property from the property set of this image. If the + * property name is not recognized, null will be returned. + * + * @param name the name of the property to get, as a + * String. + * @return a reference to the property + * Object, or the value null + */ + public Object getProperty(String name) { + name = name.toLowerCase(Locale.getDefault()); + return properties.get(name); + } + + /** + * Returns a list of the properties recognized by this image. If + * no properties are available, an empty String[] will be returned. + * + * @return an array of Strings representing valid + * property names. + */ + public String[] getPropertyNames() { + String[] names = new String[properties.size()]; + properties.keySet().toArray(names); + return names; + } + + /** + * Returns an array of Strings recognized as names by + * this property source that begin with the supplied prefix. If + * no property names match, null will be returned. + * The comparison is done in a case-independent manner. + * + *

    The default implementation calls + * getPropertyNames() and searches the list of names + * for matches. + * + * @return an array of Strings giving the valid + * property names (can be null). + */ + public String[] getPropertyNames(String prefix) { + String[] propertyNames = getPropertyNames(); +// if (propertyNames == null) { +// return null; +// } + + prefix = prefix.toLowerCase(Locale.getDefault()); + + List names = new ArrayList(); + for (String propertyName : propertyNames) { + if (propertyName.startsWith(prefix)) { + names.add(propertyName); + } + } + + if (names.size() == 0) { + return null; + } + + // Copy the strings from the Vector over to a String array. + String[] prefixNames = new String[names.size()]; + names.toArray(prefixNames); + return prefixNames; + } + + // Utility methods. + + /** + * Converts a pixel's X coordinate into a horizontal tile index + * relative to a given tile grid layout specified by its X offset + * and tile width. + */ + public static int convertXToTileX(int x, int tileGridXOffset, int tileWidth) { + x -= tileGridXOffset; + if (x < 0) { + x += 1 - tileWidth; // Force round to -infinity + } + return x / tileWidth; + } + + /** + * Converts a pixel's Y coordinate into a vertical tile index + * relative to a given tile grid layout specified by its Y offset + * and tile height. + */ + public static int convertYToTileY(int y, int tileGridYOffset, int tileHeight) { + y -= tileGridYOffset; + if (y < 0) { + y += 1 - tileHeight; // Force round to -infinity + } + return y / tileHeight; + } + + /** + * Converts a pixel's X coordinate into a horizontal tile index. + * This is a convenience method. No attempt is made to detect + * out-of-range coordinates. + * + * @param x the X coordinate of a pixel. + * @return the X index of the tile containing the pixel. + */ + public int convertXToTileX(int x) { + return convertXToTileX(x, getTileGridXOffset(), getTileWidth()); + } + + /** + * Converts a pixel's Y coordinate into a vertical tile index. + * This is a convenience method. No attempt is made to detect + * out-of-range coordinates. + * + * @param y the Y coordinate of a pixel. + * @return the Y index of the tile containing the pixel. + */ + public int convertYToTileY(int y) { + return convertYToTileY(y, getTileGridYOffset(), getTileHeight()); + } + + /** + * Converts a horizontal tile index into the X coordinate of its + * upper left pixel relative to a given tile grid layout specified + * by its X offset and tile width. + */ + public static int tileXToX(int tx, int tileGridXOffset, int tileWidth) { + return tx * tileWidth + tileGridXOffset; + } + + /** + * Converts a vertical tile index into the Y coordinate of + * its upper left pixel relative to a given tile grid layout + * specified by its Y offset and tile height. + */ + public static int tileYToY(int ty, int tileGridYOffset, int tileHeight) { + return ty * tileHeight + tileGridYOffset; + } + + /** + * Converts a horizontal tile index into the X coordinate of its + * upper left pixel. This is a convenience method. No attempt is made + * to detect out-of-range indices. + * + * @param tx the horizontal index of a tile. + * @return the X coordinate of the tile's upper left pixel. + */ + public int tileXToX(int tx) { + return tx * tileWidth + tileGridXOffset; + } + + /** + * Converts a vertical tile index into the Y coordinate of its + * upper left pixel. This is a convenience method. No attempt is made + * to detect out-of-range indices. + * + * @param ty the vertical index of a tile. + * @return the Y coordinate of the tile's upper left pixel. + */ + public int tileYToY(int ty) { + return ty * tileHeight + tileGridYOffset; + } + + public Vector getSources() { + return null; + } + + /** + * Returns the entire image in a single Raster. For images with + * multiple tiles this will require making a copy. + * + *

    The returned Raster is semantically a copy. This means + * that updates to the source image will not be reflected in the + * returned Raster. For non-writable (immutable) source images, + * the returned value may be a reference to the image's internal + * data. The returned Raster should be considered non-writable; + * any attempt to alter its pixel data (such as by casting it to + * WritableRaster or obtaining and modifying its DataBuffer) may + * result in undefined behavior. The copyData method should be + * used if the returned Raster is to be modified. + * + * @return a Raster containing a copy of this image's data. + */ + public Raster getData() { + Rectangle rect = new Rectangle(getMinX(), getMinY(), + getWidth(), getHeight()); + return getData(rect); + } + + /** + * Returns an arbitrary rectangular region of the RenderedImage + * in a Raster. The rectangle of interest will be clipped against + * the image bounds. + * + *

    The returned Raster is semantically a copy. This means + * that updates to the source image will not be reflected in the + * returned Raster. For non-writable (immutable) source images, + * the returned value may be a reference to the image's internal + * data. The returned Raster should be considered non-writable; + * any attempt to alter its pixel data (such as by casting it to + * WritableRaster or obtaining and modifying its DataBuffer) may + * result in undefined behavior. The copyData method should be + * used if the returned Raster is to be modified. + * + * @param bounds the region of the RenderedImage to be returned. + */ + public Raster getData(Rectangle bounds) { + int startX = convertXToTileX(bounds.x); + int startY = convertYToTileY(bounds.y); + int endX = convertXToTileX(bounds.x + bounds.width - 1); + int endY = convertYToTileY(bounds.y + bounds.height - 1); + Raster tile; + + if ((startX == endX) && (startY == endY)) { + tile = getTile(startX, startY); + return tile.createChild(bounds.x, bounds.y, + bounds.width, bounds.height, + bounds.x, bounds.y, null); + } else { + // Create a WritableRaster of the desired size + SampleModel sm = + sampleModel.createCompatibleSampleModel(bounds.width, + bounds.height); + + // Translate it + WritableRaster dest = + Raster.createWritableRaster(sm, bounds.getLocation()); + + for (int j = startY; j <= endY; j++) { + for (int i = startX; i <= endX; i++) { + tile = getTile(i, j); + Rectangle intersectRect = + bounds.intersection(tile.getBounds()); + Raster liveRaster = tile.createChild(intersectRect.x, + intersectRect.y, + intersectRect.width, + intersectRect.height, + intersectRect.x, + intersectRect.y, + null); + dest.setDataElements(0, 0, liveRaster); + } + } + return dest; + } + } + + /** + * Copies an arbitrary rectangular region of the RenderedImage + * into a caller-supplied WritableRaster. The region to be + * computed is determined by clipping the bounds of the supplied + * WritableRaster against the bounds of the image. The supplied + * WritableRaster must have a SampleModel that is compatible with + * that of the image. + * + *

    If the raster argument is null, the entire image will + * be copied into a newly-created WritableRaster with a SampleModel + * that is compatible with that of the image. + * + * @param dest a WritableRaster to hold the returned portion of + * the image. + * @return a reference to the supplied WritableRaster, or to a + * new WritableRaster if the supplied one was null. + */ + public WritableRaster copyData(WritableRaster dest) { + Rectangle bounds; + Raster tile; + + if (dest == null) { + bounds = getBounds(); + Point p = new Point(minX, minY); + /* A SampleModel to hold the entire image. */ + SampleModel sm = sampleModel.createCompatibleSampleModel( + width, height); + dest = Raster.createWritableRaster(sm, p); + } else { + bounds = dest.getBounds(); + } + + int startX = convertXToTileX(bounds.x); + int startY = convertYToTileY(bounds.y); + int endX = convertXToTileX(bounds.x + bounds.width - 1); + int endY = convertYToTileY(bounds.y + bounds.height - 1); + + for (int j = startY; j <= endY; j++) { + for (int i = startX; i <= endX; i++) { + tile = getTile(i, j); + Rectangle intersectRect = + bounds.intersection(tile.getBounds()); + Raster liveRaster = tile.createChild(intersectRect.x, + intersectRect.y, + intersectRect.width, + intersectRect.height, + intersectRect.x, + intersectRect.y, + null); + + /* + * WritableRaster.setDataElements takes into account of + * inRaster's minX and minY and add these to x and y. Since + * liveRaster has the origin at the correct location, the + * following call should not again give these coordinates in + * places of x and y. + */ + dest.setDataElements(0, 0, liveRaster); + } + } + return dest; + } +} diff --git a/src/main/java/org/apache/xmlgraphics/image/codec/util/SingleTileRenderedImage.java b/src/main/java/org/apache/xmlgraphics/image/codec/util/SingleTileRenderedImage.java new file mode 100644 index 0000000..e833e21 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/codec/util/SingleTileRenderedImage.java @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: SingleTileRenderedImage.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.image.codec.util; + +import java.awt.image.ColorModel; +import java.awt.image.Raster; + +// CSOFF: InnerAssignment + +/** + * A simple class that provides RenderedImage functionality + * given a Raster and a ColorModel. + * + * @version $Id: SingleTileRenderedImage.java 1732018 2016-02-24 04:51:06Z gadams $ + */ +public class SingleTileRenderedImage extends SimpleRenderedImage { + + Raster ras; + + /** + * Constructs a SingleTileRenderedImage based on a Raster + * and a ColorModel. + * + * @param ras A Raster that will define tile (0, 0) of the image. + * @param colorModel A ColorModel that will serve as the image's + * ColorModel. + */ + public SingleTileRenderedImage(Raster ras, ColorModel colorModel) { + this.ras = ras; + + this.tileGridXOffset = this.minX = ras.getMinX(); + this.tileGridYOffset = this.minY = ras.getMinY(); + this.tileWidth = this.width = ras.getWidth(); + this.tileHeight = this.height = ras.getHeight(); + this.sampleModel = ras.getSampleModel(); + this.colorModel = colorModel; + } + + /** + * Returns the image's Raster as tile (0, 0). + */ + public Raster getTile(int tileX, int tileY) { + if (tileX != 0 || tileY != 0) { + throw new IllegalArgumentException(PropertyUtil.getString("SingleTileRenderedImage0")); + } + return ras; + } +} diff --git a/src/main/java/org/apache/xmlgraphics/image/codec/util/package.html b/src/main/java/org/apache/xmlgraphics/image/codec/util/package.html new file mode 100644 index 0000000..cfb9a9f --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/codec/util/package.html @@ -0,0 +1,23 @@ + + + +org.apache.xmlgraphics.image.codec.util Package + +

    Contains utility classes for image codecs.

    + + \ No newline at end of file diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/Image.java b/src/main/java/org/apache/xmlgraphics/image/loader/Image.java new file mode 100644 index 0000000..cbaf1b0 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/Image.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: Image.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.image.loader; + +import java.awt.color.ColorSpace; +import java.awt.color.ICC_Profile; + +/** + * Represents an instance of an image (bitmap or vector graphic). The image may or may not be loaded + * into memory. Implementing classes will expose additional methods to access the actual image data. + */ +public interface Image { + + /** + * Returns an object with basic information (URI, MIME type, intrinsic size) about the image. + * @return the image information object + */ + ImageInfo getInfo(); + + /** + * Returns the image's intrinsic size. This is a shortcut for getInfo().getSize(). + * @return the image's intrinsic size + */ + ImageSize getSize(); + + /** + * Returns the flavor of the image. + * @return the image flavor + */ + ImageFlavor getFlavor(); + + /** + * Indicates whether the Image instance is cacheable in memory. + * @return true if the Image is cacheable + */ + boolean isCacheable(); + + /** + * Returns the ICC color profile if one is associated with the image. + * @return the ICC color profile or null if there's no profile + */ + ICC_Profile getICCProfile(); + + /** + * Returns the image's color space if the information is available. + * @return the color space or null if the color space is unknown or undefined + */ + ColorSpace getColorSpace(); + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/ImageContext.java b/src/main/java/org/apache/xmlgraphics/image/loader/ImageContext.java new file mode 100644 index 0000000..d4bd180 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/ImageContext.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageContext.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.image.loader; + +/** + * The ImageContext interface provides session-independent information (mainly configuration + * values). + */ +public interface ImageContext { + + /** + * Returns the resolution (in dpi) that is to be used when interpreting pixel sizes where no + * resolution information is available. + * @return the source resolution (in dpi) + */ + float getSourceResolution(); + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/ImageException.java b/src/main/java/org/apache/xmlgraphics/image/loader/ImageException.java new file mode 100644 index 0000000..ba77ab3 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/ImageException.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageException.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.image.loader; + +/** + * Base class for all image-related exceptions. + */ +public class ImageException extends Exception { + + private static final long serialVersionUID = 3785613905389979249L; + + /** + * Constructs an ImageException with the specified detail + * message. The error message string s can later be + * retrieved by the {@link java.lang.Throwable#getMessage} + * method of class java.lang.Throwable. + * + * @param s the detail message. + */ + public ImageException(String s) { + super(s); + } + + /** + * Constructs a new ImageException with the specified detail message and + * cause.

    Note that the detail message associated with + * cause is not automatically incorporated in + * this exception's detail message. + * + * @param message the detail message (which is saved for later retrieval + * by the {@link #getMessage()} method). + * @param cause the cause (which is saved for later retrieval by the + * {@link #getCause()} method). (A null value is + * permitted, and indicates that the cause is nonexistent or + * unknown.) + */ + public ImageException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/ImageFlavor.java b/src/main/java/org/apache/xmlgraphics/image/loader/ImageFlavor.java new file mode 100644 index 0000000..397a45a --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/ImageFlavor.java @@ -0,0 +1,149 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageFlavor.java 1894758 2021-11-05 13:34:47Z ssteiner $ */ + +package org.apache.xmlgraphics.image.loader; + +import org.apache.xmlgraphics.util.MimeConstants; + +/** + * The flavor of an image indicates in which form it is available. A bitmap image loaded into + * memory might be represented as a BufferedImage (indicated by ImageFlavor.BUFFERED_IMAGE). + * It is mostly used by consuming code to indicate what kind of flavors can be processed so a + * processing pipeline can do the necessary loading operations and conversions. + */ +public class ImageFlavor { + + /** An image in form of a RenderedImage instance */ + public static final ImageFlavor RENDERED_IMAGE = new ImageFlavor("RenderedImage"); + /** An image in form of a BufferedImage instance */ + public static final ImageFlavor BUFFERED_IMAGE = new SimpleRefinedImageFlavor( + RENDERED_IMAGE, "BufferedImage"); + /** An image in form of a W3C DOM instance */ + private static final ImageFlavor DOM = new ImageFlavor("DOM"); + /** An XML-based image in form of a W3C DOM instance */ + public static final ImageFlavor XML_DOM = new MimeEnabledImageFlavor(DOM, "text/xml"); + /** An image in form of a raw PNG file/stream */ + public static final ImageFlavor RAW = new ImageFlavor("Raw"); + /** An image in form of a raw PNG file/stream */ + public static final ImageFlavor RAW_PNG = new MimeEnabledImageFlavor(RAW, + MimeConstants.MIME_PNG); + /** An image in form of a raw JPEG/JFIF file/stream */ + public static final ImageFlavor RAW_JPEG = new MimeEnabledImageFlavor(RAW, + MimeConstants.MIME_JPEG); + /** An image in form of a raw TIFF file/stream */ + public static final ImageFlavor RAW_TIFF = new MimeEnabledImageFlavor(RAW, + MimeConstants.MIME_TIFF); + /** An image in form of a raw EMF (Windows Enhanced Metafile) file/stream */ + public static final ImageFlavor RAW_EMF = new MimeEnabledImageFlavor(RAW, + MimeConstants.MIME_EMF); + /** An image in form of a raw EPS (Encapsulated PostScript) file/stream */ + public static final ImageFlavor RAW_EPS = new MimeEnabledImageFlavor(RAW, + MimeConstants.MIME_EPS); + public static final ImageFlavor RAW_PDF = new MimeEnabledImageFlavor(RAW, MimeConstants.MIME_PDF); + + /** An image in form of a raw LZW file/stream */ + public static final ImageFlavor RAW_LZW = new ImageFlavor("RawLZW"); + /** An image in form of a raw CCITTFax stream */ + public static final ImageFlavor RAW_CCITTFAX = new ImageFlavor("RawCCITTFax"); + /** An image in form of a Graphics2DImage (can be painted on a Graphics2D interface) */ + public static final ImageFlavor GRAPHICS2D = new ImageFlavor("Graphics2DImage"); + + private String name; + + /** + * Constructs a new ImageFlavor. Please reuse existing constants wherever possible! + * @param name the name of the flavor (must be unique) + */ + public ImageFlavor(String name) { + this.name = name; + } + + /** + * Returns the name of the ImageFlavor. + * @return the flavor name + */ + public String getName() { + return this.name; + } + + /** + * Returns the MIME type that the image flavor represents if a MIME type is available. This + * is only applicable to images which can also exist as files. For images flavors like + * decoded in-memory images (Rendered/BufferedImage), this method will return null. + * @return the MIME type or null if no MIME type can be provided (like for in-memory images) + */ + public String getMimeType() { + return null; + } + + /** + * Returns the XML namespace URI that the image flavor represents if such a namespace URI + * is available. This is only applicable to images in XML form. Other image types will return + * null. + * @return the XML or null if no MIME type can be provided (like for in-memory images) + */ + public String getNamespace() { + return null; + } + + /** + * Indicates whether a particular image flavor is compatible with this one. + * @param flavor the other image flavor + * @return true if the two are compatible + */ + public boolean isCompatible(ImageFlavor flavor) { + return this.equals(flavor); + } + + /** {@inheritDoc} */ + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((name == null) ? 0 : name.hashCode()); + return result; + } + + /** {@inheritDoc} */ + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final ImageFlavor other = (ImageFlavor)obj; + if (name == null) { + if (other.name != null) { + return false; + } + } else if (!name.equals(other.name)) { + return false; + } + return true; + } + + /** {@inheritDoc} */ + public String toString() { + return getName(); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/ImageInfo.java b/src/main/java/org/apache/xmlgraphics/image/loader/ImageInfo.java new file mode 100644 index 0000000..59631c8 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/ImageInfo.java @@ -0,0 +1,131 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageInfo.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.image.loader; + +import java.util.Map; + +/** + * Represents an image that may not have been fully loaded. Usually, the loading only goes as far + * as necessary to know the intrinsic size of the image. The image will only fully loaded later + * when the image needs to be presented in a particular format so the consuming component can + * actually process it. The "preloading" is done so a layout engine can work with the image without + * having to fully load it (in memory). + */ +public class ImageInfo { + + /** + * Key to register the "original object" among the custom objects of an ImageInfo instance. + * @see #getOriginalImage() + */ + public static final Object ORIGINAL_IMAGE = Image.class; + + /** + * Key to register information about additional (sub-)images in the image file after the + * selected one. Valid values for this key is either a positive Integer or the constant + * {@link Boolean#TRUE} or {@link Boolean#FALSE}. A value of TRUE indicates that there are + * more subimages available but the exact number of additional images has not been determined + * for performance reasons. + */ + public static final Object HAS_MORE_IMAGES = "HAS_MORE_IMAGES"; + + /** Original URI the image was accessed with */ + private String originalURI; + /** MIME type of the image */ + private String mimeType; + + /** the image size */ + private ImageSize size; + + /** + * Map of custom objects that components further down the processing pipeline might need. + * Example: The DOM of an XML document. + */ + private Map customObjects = new java.util.HashMap(); + + /** + * Main constructor. + * @param originalURI the original URI that was specified by the user (not the resolved URI!) + * @param mimeType the MIME type of the image + */ + public ImageInfo(String originalURI, String mimeType) { + this.originalURI = originalURI; + this.mimeType = mimeType; + } + + /** + * Returns the original URI of the image. + * @return the original URI + */ + public String getOriginalURI() { + return this.originalURI; + } + + /** + * Returns the image's MIME type. + * @return the MIME type + */ + public String getMimeType() { + return this.mimeType; + } + + /** + * Returns the image's intrinsic size. + * @return the image size + */ + public ImageSize getSize() { + return this.size; + } + + /** + * Sets the image's intrinsic size. + * @param size the size + */ + public void setSize(ImageSize size) { + this.size = size; + } + + /** + * Returns a Map of custom objects associated with this instance. + * @return the Map of custom objects + */ + public Map getCustomObjects() { + return this.customObjects; + } + + /** + * Returns the original Image instance if such an Image instance is created while building + * this ImageInfo object. Some images cannot be "preloaded". They have to be fully loaded + * in order to determine the intrinsic image size. This method allows access to that fully + * loaded image so no additional re-loading has to be done later. + *

    + * This method is short for: (Image)this.customObjects.get(ORIGINAL_IMAGE); + * @return the original Image instance or null if none is set + * @see #ORIGINAL_IMAGE + */ + public Image getOriginalImage() { + return (Image)this.customObjects.get(ORIGINAL_IMAGE); + } + + /** {@inheritDoc} */ + public String toString() { + return getOriginalURI() + " (" + getMimeType() + ")"; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/ImageManager.java b/src/main/java/org/apache/xmlgraphics/image/loader/ImageManager.java new file mode 100644 index 0000000..94b7bde --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/ImageManager.java @@ -0,0 +1,426 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageManager.java 1804124 2017-08-04 14:13:54Z ssteiner $ */ + +package org.apache.xmlgraphics.image.loader; + +import java.io.IOException; +import java.util.Iterator; +import java.util.Map; + +import javax.xml.transform.Source; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.apache.xmlgraphics.image.loader.cache.ImageCache; +import org.apache.xmlgraphics.image.loader.pipeline.ImageProviderPipeline; +import org.apache.xmlgraphics.image.loader.pipeline.PipelineFactory; +import org.apache.xmlgraphics.image.loader.spi.ImageImplRegistry; +import org.apache.xmlgraphics.image.loader.spi.ImagePreloader; +import org.apache.xmlgraphics.image.loader.util.ImageUtil; +import org.apache.xmlgraphics.image.loader.util.Penalty; +import org.apache.xmlgraphics.io.XmlSourceUtil; + +/** + * ImageManager is the central starting point for image access. + */ +public class ImageManager { + + /** logger */ + protected static final Log log = LogFactory.getLog(ImageManager.class); + + /** Holds all registered interface implementations for the image package */ + private ImageImplRegistry registry; + + /** Provides session-independent information */ + private ImageContext imageContext; + + /** The image cache for this instance */ + private ImageCache cache = new ImageCache(); + + private PipelineFactory pipelineFactory = new PipelineFactory(this); + + /** + * Main constructor. + * @param context the session-independent context information + */ + public ImageManager(ImageContext context) { + this(ImageImplRegistry.getDefaultInstance(), context); + } + + /** + * Constructor for testing purposes. + * @param registry the implementation registry with all plug-ins + * @param context the session-independent context information + */ + public ImageManager(ImageImplRegistry registry, ImageContext context) { + this.registry = registry; + this.imageContext = context; + } + + /** + * Returns the ImageImplRegistry in use by the ImageManager. + * @return the ImageImplRegistry + */ + public ImageImplRegistry getRegistry() { + return this.registry; + } + + /** + * Returns the ImageContext in use by the ImageManager. + * @return the ImageContext + */ + public ImageContext getImageContext() { + return this.imageContext; + } + + /** + * Returns the ImageCache in use by the ImageManager. + * @return the ImageCache + */ + public ImageCache getCache() { + return this.cache; + } + + /** + * Returns the PipelineFactory in use by the ImageManager. + * @return the PipelineFactory + */ + public PipelineFactory getPipelineFactory() { + return this.pipelineFactory; + } + + /** + * Returns an ImageInfo object containing its intrinsic size for a given URI. The ImageInfo + * is retrieved from an image cache if it has been requested before. + * @param uri the URI of the image + * @param session the session context through which to resolve the URI if the image is not in + * the cache + * @return the ImageInfo object created from the image + * @throws ImageException If no suitable ImagePreloader can be found to load the image or + * if an error occurred while preloading the image. + * @throws IOException If an I/O error occurs while preloading the image + */ + public ImageInfo getImageInfo(String uri, ImageSessionContext session) + throws ImageException, IOException { + if (getCache() != null) { + return getCache().needImageInfo(uri, session, this); + } else { + return preloadImage(uri, session); + } + } + + /** + * Preloads an image, i.e. the format of the image is identified and some basic information + * (MIME type, intrinsic size and possibly other values) are loaded and returned as an + * ImageInfo object. Note that the image is not fully loaded normally. Only with certain formats + * the image is already fully loaded and references added to the ImageInfo's custom objects + * (see {@link ImageInfo#getOriginalImage()}). + *

    + * The reason for the preloading: Apache FOP, for example, only needs the image's intrinsic + * size during layout. Only when the document is rendered to the final format does FOP need + * to load the full image. Like this a lot of memory can be saved. + * @param uri the original URI of the image + * @param session the session context through which to resolve the URI + * @return the ImageInfo object created from the image + * @throws ImageException If no suitable ImagePreloader can be found to load the image or + * if an error occurred while preloading the image. + * @throws IOException If an I/O error occurs while preloading the image + */ + public ImageInfo preloadImage(String uri, ImageSessionContext session) + throws ImageException, IOException { + Source src = session.needSource(uri); + ImageInfo info = preloadImage(uri, src); + session.returnSource(uri, src); + return info; + } + + /** + * Preloads an image, i.e. the format of the image is identified and some basic information + * (MIME type, intrinsic size and possibly other values) are loaded and returned as an + * ImageInfo object. Note that the image is not fully loaded normally. Only with certain formats + * the image is already fully loaded and references added to the ImageInfo's custom objects + * (see {@link ImageInfo#getOriginalImage()}). + *

    + * The reason for the preloading: Apache FOP, for example, only needs the image's intrinsic + * size during layout. Only when the document is rendered to the final format does FOP need + * to load the full image. Like this a lot of memory can be saved. + * @param uri the original URI of the image + * @param src the Source object to load the image from + * @return the ImageInfo object created from the image + * @throws ImageException If no suitable ImagePreloader can be found to load the image or + * if an error occurred while preloading the image. + * @throws IOException If an I/O error occurs while preloading the image + */ + public ImageInfo preloadImage(String uri, Source src) + throws ImageException, IOException { + Iterator iter = registry.getPreloaderIterator(); + while (iter.hasNext()) { + ImagePreloader preloader = (ImagePreloader) iter.next(); + ImageInfo info = preloader.preloadImage(uri, src, imageContext); + if (info != null) { + return info; + } + } + throw new ImageException("The file format is not supported. No ImagePreloader found for " + + uri); + } + + private Map prepareHints(Map hints, ImageSessionContext sessionContext) { + Map newHints = new java.util.HashMap(); + if (hints != null) { + newHints.putAll(hints); //Copy in case an unmodifiable map is passed in + } + if (!newHints.containsKey(ImageProcessingHints.IMAGE_SESSION_CONTEXT) + && sessionContext != null) { + newHints.put(ImageProcessingHints.IMAGE_SESSION_CONTEXT, sessionContext); + + } + if (!newHints.containsKey(ImageProcessingHints.IMAGE_MANAGER)) { + newHints.put(ImageProcessingHints.IMAGE_MANAGER, this); + } + return newHints; + } + + /** + * Loads an image. The caller can indicate what kind of image flavor is requested. When this + * method is called the code looks for a suitable ImageLoader and, if necessary, builds + * a conversion pipeline so it can return the image in exactly the form the caller needs. + *

    + * Optionally, it is possible to pass in Map of hints. These hints may be used by ImageLoaders + * and ImageConverters to act on the image. See {@link ImageProcessingHints} for common hints + * used by the bundled implementations. You can, of course, define your own hints. + * @param info the ImageInfo instance for the image (obtained by + * {@link #getImageInfo(String, ImageSessionContext)}) + * @param flavor the requested image flavor. + * @param hints a Map of hints to any of the background components or null + * @param session the session context + * @return the fully loaded image + * @throws ImageException If no suitable loader/converter combination is available to fulfill + * the request or if an error occurred while loading the image. + * @throws IOException If an I/O error occurs + */ + public Image getImage(ImageInfo info, ImageFlavor flavor, Map hints, + ImageSessionContext session) + throws ImageException, IOException { + hints = prepareHints(hints, session); + + Image img = null; + ImageProviderPipeline pipeline = getPipelineFactory().newImageConverterPipeline( + info, flavor); + if (pipeline != null) { + img = pipeline.execute(info, hints, session); + } + if (img == null) { + throw new ImageException( + "Cannot load image (no suitable loader/converter combination available) for " + + info); + } + XmlSourceUtil.closeQuietly(session.getSource(info.getOriginalURI())); + return img; + } + + /** + * Loads an image. The caller can indicate what kind of image flavors are requested. When this + * method is called the code looks for a suitable ImageLoader and, if necessary, builds + * a conversion pipeline so it can return the image in exactly the form the caller needs. + * The array of image flavors is ordered, so the first image flavor is given highest priority. + *

    + * Optionally, it is possible to pass in Map of hints. These hints may be used by ImageLoaders + * and ImageConverters to act on the image. See {@link ImageProcessingHints} for common hints + * used by the bundled implementations. You can, of course, define your own hints. + * @param info the ImageInfo instance for the image (obtained by + * {@link #getImageInfo(String, ImageSessionContext)}) + * @param flavors the requested image flavors (in preferred order). + * @param hints a Map of hints to any of the background components or null + * @param session the session context + * @return the fully loaded image + * @throws ImageException If no suitable loader/converter combination is available to fulfill + * the request or if an error occurred while loading the image. + * @throws IOException If an I/O error occurs + */ + public Image getImage(ImageInfo info, ImageFlavor[] flavors, Map hints, + ImageSessionContext session) + throws ImageException, IOException { + hints = prepareHints(hints, session); + + Image img = null; + ImageProviderPipeline[] candidates = getPipelineFactory().determineCandidatePipelines( + info, flavors); + ImageProviderPipeline pipeline = choosePipeline(candidates); + + if (pipeline != null) { + img = pipeline.execute(info, hints, session); + } + if (img == null) { + throw new ImageException( + "Cannot load image (no suitable loader/converter combination available) for " + + info); + } + XmlSourceUtil.closeQuietly(session.getSource(info.getOriginalURI())); + return img; + } + + /** + * Loads an image with no hints. See + * {@link #getImage(ImageInfo, ImageFlavor, Map, ImageSessionContext)} for more + * information. + * @param info the ImageInfo instance for the image (obtained by + * {@link #getImageInfo(String, ImageSessionContext)}) + * @param flavor the requested image flavor. + * @param session the session context + * @return the fully loaded image + * @throws ImageException If no suitable loader/converter combination is available to fulfill + * the request or if an error occurred while loading the image. + * @throws IOException If an I/O error occurs + */ + public Image getImage(ImageInfo info, ImageFlavor flavor, ImageSessionContext session) + throws ImageException, IOException { + return getImage(info, flavor, ImageUtil.getDefaultHints(session), session); + } + + /** + * Loads an image with no hints. See + * {@link #getImage(ImageInfo, ImageFlavor[], Map, ImageSessionContext)} for more + * information. + * @param info the ImageInfo instance for the image (obtained by + * {@link #getImageInfo(String, ImageSessionContext)}) + * @param flavors the requested image flavors (in preferred order). + * @param session the session context + * @return the fully loaded image + * @throws ImageException If no suitable loader/converter combination is available to fulfill + * the request or if an error occurred while loading the image. + * @throws IOException If an I/O error occurs + */ + public Image getImage(ImageInfo info, ImageFlavor[] flavors, ImageSessionContext session) + throws ImageException, IOException { + return getImage(info, flavors, ImageUtil.getDefaultHints(session), session); + } + + /** + * Closes the resources associated to the given image. This method should be + * used only when none of the {@code getImage} methods is called by the + * client application. + * + * @param uri the URI of the image + * @param session the session context that was used to resolve the URI + */ + public void closeImage(String uri, ImageSessionContext session) { + XmlSourceUtil.closeQuietly(session.getSource(uri)); + } + + /** + * Converts an image. The caller can indicate what kind of image flavors are requested. When + * this method is called the code looks for a suitable combination of ImageConverters so it + * can return the image in exactly the form the caller needs. + * The array of image flavors is ordered, so the first image flavor is given highest priority. + *

    + * Optionally, it is possible to pass in Map of hints. These hints may be used by + * ImageConverters to act on the image. See {@link ImageProcessingHints} for common hints + * used by the bundled implementations. You can, of course, define your own hints. + * @param image the image to convert + * @param flavors the requested image flavors (in preferred order). + * @param hints a Map of hints to any of the background components or null + * @return the fully loaded image + * @throws ImageException If no suitable loader/converter combination is available to fulfill + * the request or if an error occurred while loading the image. + * @throws IOException If an I/O error occurs + */ + public Image convertImage(Image image, ImageFlavor[] flavors, Map hints) + throws ImageException, IOException { + hints = prepareHints(hints, null); + ImageInfo info = image.getInfo(); + + Image img = null; + for (ImageFlavor flavor : flavors) { + if (image.getFlavor().equals(flavor)) { + //Shortcut (the image is already in one of the requested formats) + return image; + } + } + ImageProviderPipeline[] candidates = getPipelineFactory().determineCandidatePipelines( + image, flavors); + ImageProviderPipeline pipeline = choosePipeline(candidates); + + if (pipeline != null) { + img = pipeline.execute(info, image, hints, null); + } + if (img == null) { + throw new ImageException( + "Cannot convert image " + image + + " (no suitable converter combination available)"); + } + return img; + } + + /** + * Converts an image with no hints. See + * {@link #convertImage(Image, ImageFlavor[], Map)} for more + * information. + * @param image the image to convert + * @param flavors the requested image flavors (in preferred order). + * @return the fully loaded image + * @throws ImageException If no suitable loader/converter combination is available to fulfill + * the request or if an error occurred while loading the image. + * @throws IOException If an I/O error occurs + */ + public Image convertImage(Image image, ImageFlavor[] flavors) + throws ImageException, IOException { + return convertImage(image, flavors, null); + } + + /** + * Chooses the best {@link ImageProviderPipeline} from a set of candidates. + * @param candidates the candidates + * @return the best pipeline + */ + public ImageProviderPipeline choosePipeline(ImageProviderPipeline[] candidates) { + ImageProviderPipeline pipeline = null; + int minPenalty = Integer.MAX_VALUE; + int count = candidates.length; + if (log.isTraceEnabled()) { + log.trace("Candidate Pipelines:"); + for (int i = 0; i < count; i++) { + if (candidates[i] == null) { + continue; + } + log.trace(" " + i + ": " + + candidates[i].getConversionPenalty(getRegistry()) + " for " + candidates[i]); + } + } + for (int i = count - 1; i >= 0; i--) { + if (candidates[i] == null) { + continue; + } + Penalty penalty = candidates[i].getConversionPenalty(getRegistry()); + if (penalty.isInfinitePenalty()) { + continue; //Exclude candidate on infinite penalty + } + if (penalty.getValue() <= minPenalty) { + pipeline = candidates[i]; + minPenalty = penalty.getValue(); + } + } + if (log.isDebugEnabled()) { + log.debug("Chosen pipeline: " + pipeline); + } + return pipeline; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/ImageProcessingHints.java b/src/main/java/org/apache/xmlgraphics/image/loader/ImageProcessingHints.java new file mode 100644 index 0000000..2ca7a35 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/ImageProcessingHints.java @@ -0,0 +1,86 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageProcessingHints.java 924666 2010-03-18 08:26:30Z jeremias $ */ + +package org.apache.xmlgraphics.image.loader; + +/** + * This interface defines some standard hints to be used for image processing in this package. + * They are provided for convenience. You can define your own hints as you like. + * Generally, consumers should not rely on the presence of any hint! + */ +public interface ImageProcessingHints { + + /** Used to send a hint about the source resolution for pixel to unit conversions. */ + Object SOURCE_RESOLUTION = "SOURCE_RESOLUTION"; //Value: Number (unit dpi) + /** Used to send a hint about the target resolution (of the final output format). */ + Object TARGET_RESOLUTION = "TARGET_RESOLUTION"; //Value: Number (unit dpi) + + /** + * Used to pass in the {@link ImageSessionContext}. A consumer can use this to load embedded + * images over the same mechanism as the main image (ex. JPEG images referenced in an + * SVG image). + * @since 1.4 + */ + Object IMAGE_SESSION_CONTEXT = "IMAGE_SESSION_CONTEXT"; //Value: ImageSessionContext instance + + /** + * Used to pass in the {@link ImageManager}. A consumer can use this to load embedded + * images over the same mechanism as the main image (ex. JPEG images referenced in an + * SVG image). + * @since 1.4 + */ + Object IMAGE_MANAGER = "IMAGE_MANAGER"; //Value: ImageManager instance + + /** Used to tell the image loader to ignore any color profile in the image. */ + Object IGNORE_COLOR_PROFILE = "IGNORE_COLOR_PROFILE"; //Value: Boolean + + /** Used to tell a bitmap producer to generate a certain type of bitmap. */ + Object BITMAP_TYPE_INTENT = "BITMAP_TYPE_INTENT"; + + /** + * Used with BITMAP_TYPE_INTENT to indicate that the generated bitmap should be a + * grayscale image. + */ + String BITMAP_TYPE_INTENT_GRAY = "gray"; + + /** + * Used with BITMAP_TYPE_INTENT to indicate that the generated bitmap should be a + * 1 bit black and white image. + */ + String BITMAP_TYPE_INTENT_MONO = "mono"; + + /** + * Used to indicate how existing transparency information (for example, an alpha channel) + * shall be treated. */ + Object TRANSPARENCY_INTENT = "TRANSPARENCY_INTENT"; + + /** + * Used with TRANSPARENCY_INTENT to indicate that any transparency information shall be + * preserved (the default). + */ + String TRANSPARENCY_INTENT_PRESERVE = "preserve"; + + /** + * Used with TRANSPARENCY_INTENT to indicate that any transparency information shall be + * ignored. + */ + String TRANSPARENCY_INTENT_IGNORE = "ignore"; + + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/ImageSessionContext.java b/src/main/java/org/apache/xmlgraphics/image/loader/ImageSessionContext.java new file mode 100644 index 0000000..b73a486 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/ImageSessionContext.java @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageSessionContext.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.image.loader; + +import java.io.FileNotFoundException; + +import javax.xml.transform.Source; + + +/** + * This interface is used to tell the cache which images are used by a session (in FOP that would + * be a rendering run). Images access within a session get a hard reference so they cannot be + * discarded. That could increase memory usage but helps with performance because the images + * don't get unloaded between layout and rendering which would mean that they have to be reloaded. + */ +public interface ImageSessionContext { + + /** + * Returns the session-independent context object which provides configuration information. + * @return the associated ImageContext instance + */ + ImageContext getParentContext(); + + /** + * Returns the resolution (in dpi) of the target device used when painting images. + * @return the target resolution (in dpi) + */ + float getTargetResolution(); + + /** + * Attempts to create a Source object from the given URI. If possible this method returns + * an ImageSource instance which provides the best possible method to access the image. + * @param uri URI to access + * @return A {@link javax.xml.transform.Source} object, or null if the URI + * cannot be resolved. + */ + Source newSource(String uri); + + /** + * Returns a Source object for a URI. This method is not guaranteed to return an instance. + * Implementations normally return already created Sources from a pool (normally populated + * through the {@link #returnSource(String, Source)} method). + * @param uri the URI of the image + * @return the Source object to load the image from, or null + */ + Source getSource(String uri); + + /** + * Returns a Source object for a URI. This method is guaranteed to return a Source object. If + * the image cannot be found, a {@link FileNotFoundException} is thrown. + * @param uri the URI of the image + * @return the Source object to load the image from + * @throws FileNotFoundException if the image cannot be found + */ + Source needSource(String uri) throws FileNotFoundException; + + /** + * Returns a Source object to a pool. This is provided in order to reuse a Source object + * between the preloading and the final loading of an image. Note that not all Source objects + * can be reused! Non-reusable Sources are discarded. + * @param uri the URI of the image + * @param src the Source object belonging to the URI + */ + void returnSource(String uri, Source src); + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/ImageSize.java b/src/main/java/org/apache/xmlgraphics/image/loader/ImageSize.java new file mode 100644 index 0000000..e386125 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/ImageSize.java @@ -0,0 +1,242 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageSize.java 696968 2008-09-19 07:52:04Z jeremias $ */ + +package org.apache.xmlgraphics.image.loader; + +import java.awt.Dimension; +import java.awt.geom.Dimension2D; + +import org.apache.xmlgraphics.java2d.Dimension2DDouble; +import org.apache.xmlgraphics.util.UnitConv; + +/** + * Encapsulates the size of an image. + */ +public class ImageSize { + + private int widthPx; + private int heightPx; + + private int widthMpt; + private int heightMpt; + private int baselinePositionFromBottomMpt; + + private double dpiHorizontal; + private double dpiVertical; + + + /** + * Constructor. + * @param widthPx the width of the image in pixels + * @param heightPx the height of the image in pixels + * @param dpiHorizontal the horizontal resolution in dots per inch + * @param dpiVertical the vertical resolution in dots per inch + */ + public ImageSize(int widthPx, int heightPx, double dpiHorizontal, double dpiVertical) { + setSizeInPixels(widthPx, heightPx); + setResolution(dpiHorizontal, dpiVertical); + } + + /** + * Constructor. + * @param widthPx the width of the image in pixels + * @param heightPx the height of the image in pixels + * @param dpi the resolution in dots per inch + */ + public ImageSize(int widthPx, int heightPx, double dpi) { + this(widthPx, heightPx, dpi, dpi); + } + + /** + * Default Constructor. + */ + public ImageSize() { + //nop + } + + /** + * Sets the image's size in pixels. + * @param width the width in pixels + * @param height the height in pixels + */ + public void setSizeInPixels(int width, int height) { + this.widthPx = width; + this.heightPx = height; + } + + /** + * Sets the image's size in millipoints. + * @param width the width in millipoints + * @param height the height in millipoints + */ + public void setSizeInMillipoints(int width, int height) { + this.widthMpt = width; + this.heightMpt = height; + } + + /** + * Sets the image's resolution for interpreting the pixel size. + * @param horizontal the horizontal resolution in dpi + * @param vertical the vertical resolution in dpi + */ + public void setResolution(double horizontal, double vertical) { + this.dpiHorizontal = horizontal; + this.dpiVertical = vertical; + } + + /** + * Sets the image's resolution for interpreting the pixel size. + * @param resolution the resolution in dpi + */ + public void setResolution(double resolution) { + setResolution(resolution, resolution); + } + + /** + * Sets the vertical position of the baseline of the image relative to the bottom of the image. + * The default is 0mpt (i.e. the image is bottom-aligned). This is used for MathML images, for + * example, which have a baseline. Using the value the images can be properly aligned with + * other text. Most other image don't have an implicit baseline. + * @param distance the distance from the bottom of the image in millipoints + */ + public void setBaselinePositionFromBottom(int distance) { + this.baselinePositionFromBottomMpt = distance; + } + + /** + * Returns the vertical position of the baseline of the image relative to the bottom of the + * image. The default is 0mpt (i.e. the image is bottom-aligned). This is used for MathML + * images, for example, which have a baseline. Using the value the images can be properly + * aligned with other text. Most other image don't have an implicit baseline. + * @return the distance from the bottom of the image in millipoints + */ + public int getBaselinePositionFromBottom() { + return this.baselinePositionFromBottomMpt; + } + + /** + * Returns the image's width in pixels. + * @return the width in pixels + */ + public int getWidthPx() { + return widthPx; + } + + /** + * Returns the image's height in pixels. + * @return the height in pixels + */ + public int getHeightPx() { + return heightPx; + } + + /** + * Returns the image's width in millipoints. + * @return the width in millipoints + */ + public int getWidthMpt() { + return widthMpt; + } + + /** + * Returns the image's height in millipoints. + * @return the height in millipoints + */ + public int getHeightMpt() { + return heightMpt; + } + + /** + * Returns the image's horizontal resolution in dpi (dots per inch). + * @return the horizontal resolution in dpi + */ + public double getDpiHorizontal() { + return dpiHorizontal; + } + + /** + * Returns the image's vertical resolution in dpi (dots per inch). + * @return the vertical resolution in dpi + */ + public double getDpiVertical() { + return dpiVertical; + } + + /** + * Returns the size in millipoints as a Dimension object. + * @return the size in millipoints + */ + public Dimension getDimensionMpt() { + return new Dimension(getWidthMpt(), getHeightMpt()); + } + + /** + * Returns the size in points as a Dimension2D object. + * @return the size in points + */ + public Dimension2D getDimensionPt() { + return new Dimension2DDouble(getWidthMpt() / 1000.0, getHeightMpt() / 1000.0); + } + + /** + * Returns the size in pixels as a Dimension object. + * @return the size in pixels + */ + public Dimension getDimensionPx() { + return new Dimension(getWidthPx(), getHeightPx()); + } + + private void checkResolutionAvailable() { + if (this.dpiHorizontal == 0 || this.dpiVertical == 0) { + throw new IllegalStateException("The resolution must be set"); + } + } + + /** + * Calculates the size in millipoints based on the size in pixels and the resolution. + */ + public void calcSizeFromPixels() { + checkResolutionAvailable(); + this.widthMpt = (int)Math.round(UnitConv.in2mpt(this.widthPx / this.dpiHorizontal)); + this.heightMpt = (int)Math.round(UnitConv.in2mpt(this.heightPx / this.dpiVertical)); + } + + /** + * Calculates the size in pixels based on the size in millipoints and the resolution. + */ + public void calcPixelsFromSize() { + checkResolutionAvailable(); + this.widthPx = (int)Math.round(UnitConv.mpt2in(this.widthMpt * this.dpiHorizontal)); + this.heightPx = (int)Math.round(UnitConv.mpt2in(this.heightMpt * this.dpiVertical)); + } + + /** {@inheritDoc} */ + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append("Size: "); + sb.append(getWidthMpt()).append('x').append(getHeightMpt()).append(" mpt"); + sb.append(" ("); + sb.append(getWidthPx()).append('x').append(getHeightPx()).append(" px"); + sb.append(" at ").append(getDpiHorizontal()).append('x').append(getDpiVertical()); + sb.append(" dpi"); + sb.append(")"); + return sb.toString(); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/ImageSource.java b/src/main/java/org/apache/xmlgraphics/image/loader/ImageSource.java new file mode 100644 index 0000000..4be4419 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/ImageSource.java @@ -0,0 +1,104 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageSource.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.image.loader; + +import java.io.InputStream; + +import javax.imageio.stream.ImageInputStream; +import javax.xml.transform.Source; + +import org.apache.xmlgraphics.image.loader.util.ImageInputStreamAdapter; + +/** + * Acts as a holder for the input to image loading operations. + */ +public class ImageSource implements Source { + + private String systemId; + private ImageInputStream iin; + private boolean fastSource; + + /** + * Main constructor. + * @param in the ImageInputStream to load from + * @param systemId the system identifier (resolved URI) of the image + * @param fastSource true if it's a fast source (accessing local files) + */ + public ImageSource(ImageInputStream in, String systemId, boolean fastSource) { + assert in != null : "InputStream is null"; + this.iin = in; + this.systemId = systemId; + this.fastSource = fastSource; + } + + /** + * Returns an InputStream which operates on the underlying ImageInputStream. + * @return the InputStream or null if the stream has been closed + */ + public InputStream getInputStream() { + if (this.iin == null) { + return null; + } else { + return new ImageInputStreamAdapter(this.iin); + } + } + + /** + * Returns the ImageInputStream. + * @return the ImageInputStream or null if the stream has been closed + */ + public ImageInputStream getImageInputStream() { + return this.iin; + } + + /** + * Sets the ImageInputStream. + * @param in the ImageInputStream + */ + public void setImageInputStream(ImageInputStream in) { + this.iin = in; + } + + /** {@inheritDoc} */ + public String getSystemId() { + return this.systemId; + } + + /** {@inheritDoc} */ + public void setSystemId(String systemId) { + this.systemId = systemId; + } + + /** + * Indicates whether this ImageSource is a fast source, i.e. accesses local files rather than + * network resources. + * @return true if it's a fast source + */ + public boolean isFastSource() { + return this.fastSource; + } + + /** {@inheritDoc} */ + public String toString() { + return (isFastSource() ? "FAST " : "") + "ImageSource: " + + getSystemId() + " " + getImageInputStream(); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/MimeEnabledImageFlavor.java b/src/main/java/org/apache/xmlgraphics/image/loader/MimeEnabledImageFlavor.java new file mode 100644 index 0000000..51e8375 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/MimeEnabledImageFlavor.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: MimeEnabledImageFlavor.java 1731775 2016-02-23 01:13:36Z gadams $ */ + +package org.apache.xmlgraphics.image.loader; + +/** + * Special image flavor subclass which enables the restriction to a particular MIME type. + */ +public class MimeEnabledImageFlavor extends RefinedImageFlavor { + + private String mime; + + /** + * Constructs a new image flavor. + * @param parentFlavor the parent image flavor + * @param mime a MIME type refining the parent image flavor + */ + public MimeEnabledImageFlavor(ImageFlavor parentFlavor, String mime) { + super(mime + ";" + parentFlavor.getName(), parentFlavor); + this.mime = mime; + } + + /** {@inheritDoc} */ + public String getMimeType() { + return this.mime; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + + MimeEnabledImageFlavor that = (MimeEnabledImageFlavor) o; + + if (mime != null ? !mime.equals(that.mime) : that.mime != null) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + (mime != null ? mime.hashCode() : 0); + return result; + } +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/RefinedImageFlavor.java b/src/main/java/org/apache/xmlgraphics/image/loader/RefinedImageFlavor.java new file mode 100644 index 0000000..dfbb699 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/RefinedImageFlavor.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: RefinedImageFlavor.java 734638 2009-01-15 09:19:32Z jeremias $ */ + +package org.apache.xmlgraphics.image.loader; + +/** + * Special image flavor subclass which enables the refinement to specific (sub-)flavors but + * maintaining compatibility to a parent (i.e. more general) flavor. + */ +public abstract class RefinedImageFlavor extends ImageFlavor { + + private ImageFlavor parentFlavor; + + /** + * Constructs a new image flavor. + * @param parentFlavor the parent image flavor + */ + protected RefinedImageFlavor(ImageFlavor parentFlavor) { + this(parentFlavor.getName(), parentFlavor); + } + + /** + * Constructs a new image flavor. + * @param parentFlavor the parent image flavor + * @param name the name of the flavor (must be unique) + */ + protected RefinedImageFlavor(String name, ImageFlavor parentFlavor) { + super(name); + this.parentFlavor = parentFlavor; + } + + /** + * Returns the associated parent image flavor. + * @return the parent image flavor + */ + public ImageFlavor getParentFlavor() { + return this.parentFlavor; + } + + /** {@inheritDoc} */ + public String getMimeType() { + return this.parentFlavor.getMimeType(); + } + + /** {@inheritDoc} */ + public String getNamespace() { + return this.parentFlavor.getNamespace(); + } + + /** {@inheritDoc} */ + public boolean isCompatible(ImageFlavor flavor) { + return getParentFlavor().isCompatible(flavor) + || super.isCompatible(flavor); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/SimpleRefinedImageFlavor.java b/src/main/java/org/apache/xmlgraphics/image/loader/SimpleRefinedImageFlavor.java new file mode 100644 index 0000000..309e9c3 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/SimpleRefinedImageFlavor.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: SimpleRefinedImageFlavor.java 682720 2008-08-05 14:22:29Z jeremias $ */ + +package org.apache.xmlgraphics.image.loader; + +/** + * Simple refined image flavor implementation that just differs flavors by name but allows to + * specify a parent flavor. + */ +public class SimpleRefinedImageFlavor extends RefinedImageFlavor { + + /** + * Main constructor. + * @param parentFlavor the parent image flavor + * @param name the name of the image flavor + */ + public SimpleRefinedImageFlavor(ImageFlavor parentFlavor, String name) { + super(name, parentFlavor); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/SubImageNotFoundException.java b/src/main/java/org/apache/xmlgraphics/image/loader/SubImageNotFoundException.java new file mode 100644 index 0000000..f97bed0 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/SubImageNotFoundException.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: SubImageNotFoundException.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.image.loader; + +/** + * Exception which indicates that a particular (sub-)image could not be found. + */ +public class SubImageNotFoundException extends ImageException { + + private static final long serialVersionUID = 3785613905389979249L; + + /** + * Constructs an ImageException with the specified detail + * message. The error message string s can later be + * retrieved by the {@link java.lang.Throwable#getMessage} + * method of class java.lang.Throwable. + * + * @param s the detail message. + */ + public SubImageNotFoundException(String s) { + super(s); + } + + /** + * Constructs a new ImageException with the specified detail message and + * cause.

    Note that the detail message associated with + * cause is not automatically incorporated in + * this exception's detail message. + * + * @param message the detail message (which is saved for later retrieval + * by the {@link #getMessage()} method). + * @param cause the cause (which is saved for later retrieval by the + * {@link #getCause()} method). (A null value is + * permitted, and indicates that the cause is nonexistent or + * unknown.) + */ + public SubImageNotFoundException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/XMLNamespaceEnabledImageFlavor.java b/src/main/java/org/apache/xmlgraphics/image/loader/XMLNamespaceEnabledImageFlavor.java new file mode 100644 index 0000000..67665dd --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/XMLNamespaceEnabledImageFlavor.java @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: XMLNamespaceEnabledImageFlavor.java 1731775 2016-02-23 01:13:36Z gadams $ */ + +package org.apache.xmlgraphics.image.loader; + +/** + * Special image flavor subclass which enables the restriction to a particular XML namespace. + */ +public class XMLNamespaceEnabledImageFlavor extends RefinedImageFlavor { + + /** An XML-based SVG image in form of a W3C DOM instance */ + public static final ImageFlavor SVG_DOM = new XMLNamespaceEnabledImageFlavor( + ImageFlavor.XML_DOM, "http://www.w3.org/2000/svg"); + + private String namespace; + + /** + * Constructs a new image flavor. + * @param parentFlavor the parent image flavor + * @param namespace an XML namespace URI refining the parent image flavor + */ + public XMLNamespaceEnabledImageFlavor(ImageFlavor parentFlavor, String namespace) { + super(parentFlavor.getName() + ";namespace=" + namespace, parentFlavor); + this.namespace = namespace; + } + + /** {@inheritDoc} */ + public String getNamespace() { + return this.namespace; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + + XMLNamespaceEnabledImageFlavor that = (XMLNamespaceEnabledImageFlavor) o; + + if (namespace != null ? !namespace.equals(that.namespace) : that.namespace != null) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + (namespace != null ? namespace.hashCode() : 0); + return result; + } +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/cache/DefaultExpirationPolicy.java b/src/main/java/org/apache/xmlgraphics/image/loader/cache/DefaultExpirationPolicy.java new file mode 100644 index 0000000..d2a238e --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/cache/DefaultExpirationPolicy.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DefaultExpirationPolicy.java 1681108 2015-05-22 13:26:12Z ssteiner $ */ + +package org.apache.xmlgraphics.image.loader.cache; + +/** + * Implements the default expiration policy for the image cache. + */ +public class DefaultExpirationPolicy implements ExpirationPolicy { + + public static final int EXPIRATION_IMMEDIATE = 0; + public static final int EXPIRATION_NEVER = -1; + + private int expirationAfter; //in seconds + + /** + * Creates a new policy with default settings (expiration in 60 seconds). + */ + public DefaultExpirationPolicy() { + this(60); + } + + /** + * Creates a new policy. + * @param expirationAfter the expiration in seconds (a negative value means: never expire) + */ + public DefaultExpirationPolicy(int expirationAfter) { + this.expirationAfter = expirationAfter; + } + + private boolean isNeverExpired() { + return (this.expirationAfter < 0); + } + + /** {@inheritDoc} */ + public boolean isExpired(TimeStampProvider provider, long timestamp) { + if (isNeverExpired()) { + return false; + } else { + long now = provider.getTimeStamp(); + return now >= (timestamp + (this.expirationAfter * 1000L)); + } + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/cache/ExpirationPolicy.java b/src/main/java/org/apache/xmlgraphics/image/loader/cache/ExpirationPolicy.java new file mode 100644 index 0000000..e7a294e --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/cache/ExpirationPolicy.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ExpirationPolicy.java 759150 2009-03-27 14:21:45Z jeremias $ */ + +package org.apache.xmlgraphics.image.loader.cache; + +/** + * Represents an expiration policy for cache entries that have a creation time stamp. + */ +public interface ExpirationPolicy { + + /** + * Indicates whether a cache entry is expired given its creation time stamp. + * @param provider the provider for new time stamps + * @param timestamp the creation time stamp (the semantics of + * {@link System#currentTimeMillis()} apply) + * @return true if the entry is to be considered expired, false if not + */ + boolean isExpired(TimeStampProvider provider, long timestamp); + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/cache/ImageCache.java b/src/main/java/org/apache/xmlgraphics/image/loader/cache/ImageCache.java new file mode 100644 index 0000000..098a5dd --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/cache/ImageCache.java @@ -0,0 +1,301 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageCache.java 1804124 2017-08-04 14:13:54Z ssteiner $ */ + +package org.apache.xmlgraphics.image.loader.cache; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import javax.xml.transform.Source; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.apache.xmlgraphics.image.loader.Image; +import org.apache.xmlgraphics.image.loader.ImageException; +import org.apache.xmlgraphics.image.loader.ImageFlavor; +import org.apache.xmlgraphics.image.loader.ImageInfo; +import org.apache.xmlgraphics.image.loader.ImageManager; +import org.apache.xmlgraphics.image.loader.ImageSessionContext; +import org.apache.xmlgraphics.image.loader.util.SoftMapCache; + + +/** + * This class provides a cache for images. The main key into the images is the original URI the + * image was accessed with. + *

    + * Don't use one ImageCache instance in the context of multiple base URIs because relative URIs + * would not work correctly anymore. + *

    + * By default, the URIs of inaccessible images are remembered but these entries are discarded + * after 60 seconds (which causes a retry next time the same URI is requested). This allows + * to counteract performance loss when accessing invalid or temporarily unavailable images + * over slow connections. + */ +public class ImageCache { + + /** logger */ + protected static final Log log = LogFactory.getLog(ImageCache.class); + + //Handling of invalid URIs + private Map invalidURIs = Collections.synchronizedMap(new java.util.HashMap()); + private ExpirationPolicy invalidURIExpirationPolicy; + + //Actual image cache + private SoftMapCache imageInfos = new SoftMapCache(true); + private SoftMapCache images = new SoftMapCache(true); + + private ImageCacheListener cacheListener; + private TimeStampProvider timeStampProvider; + private long lastHouseKeeping; + + /** + * Default constructor with default settings. + */ + public ImageCache() { + this(new TimeStampProvider(), new DefaultExpirationPolicy()); + } + + /** + * Constructor for customized behaviour and testing. + * @param timeStampProvider the time stamp provider to use + * @param invalidURIExpirationPolicy the expiration policy for invalid URIs + */ + public ImageCache(TimeStampProvider timeStampProvider, + ExpirationPolicy invalidURIExpirationPolicy) { + this.timeStampProvider = timeStampProvider; + this.invalidURIExpirationPolicy = invalidURIExpirationPolicy; + this.lastHouseKeeping = this.timeStampProvider.getTimeStamp(); + } + + /** + * Sets an ImageCacheListener instance so the events in the image cache can be observed. + * @param listener the listener instance + */ + public void setCacheListener(ImageCacheListener listener) { + this.cacheListener = listener; + } + + /** + * Returns an ImageInfo instance for a given URI. + * @param uri the image's URI + * @param session the session context + * @param manager the ImageManager handling the images + * @return the ImageInfo instance + * @throws ImageException if an error occurs while parsing image data + * @throws IOException if an I/O error occurs while loading image data + */ + public ImageInfo needImageInfo(String uri, ImageSessionContext session, ImageManager manager) + throws ImageException, IOException { + //Fetch unique version of the URI and use it for synchronization so we have some sort of + //"row-level" locking instead of "table-level" locking (to use a database analogy). + //The fine locking strategy is necessary since preloading an image is a potentially long + //operation. + if (isInvalidURI(uri)) { + throw new FileNotFoundException("Image not found: " + uri); + } + String lockURI = uri.intern(); + synchronized (lockURI) { + ImageInfo info = getImageInfo(uri); + if (info == null) { + try { + Source src = session.needSource(uri); + if (src == null) { + registerInvalidURI(uri); + throw new FileNotFoundException("Image not found: " + uri); + } + info = manager.preloadImage(uri, src); + session.returnSource(uri, src); + } catch (IOException ioe) { + registerInvalidURI(uri); + throw ioe; + } catch (ImageException e) { + registerInvalidURI(uri); + throw e; + } + if (info.getOriginalImage() == null || info.getOriginalImage().isCacheable()) { + putImageInfo(info); + } + } + return info; + } + } + + /** + * Indicates whether a URI has previously been identified as an invalid URI. + * @param uri the image's URI + * @return true if the URI is invalid + */ + public boolean isInvalidURI(String uri) { + boolean expired = removeInvalidURIIfExpired(uri); + if (expired) { + return false; + } else { + if (cacheListener != null) { + cacheListener.invalidHit(uri); + } + return true; + } + } + + private boolean removeInvalidURIIfExpired(String uri) { + Long timestamp = (Long) invalidURIs.get(uri); + boolean expired = (timestamp == null) + || this.invalidURIExpirationPolicy.isExpired( + this.timeStampProvider, timestamp); + if (expired) { + this.invalidURIs.remove(uri); + } + return expired; + } + + /** + * Returns an ImageInfo instance from the cache or null if none is found. + * @param uri the image's URI + * @return the ImageInfo instance or null if the requested information is not in the cache + */ + protected ImageInfo getImageInfo(String uri) { + ImageInfo info = (ImageInfo)imageInfos.get(uri); + if (cacheListener != null) { + if (info != null) { + cacheListener.cacheHitImageInfo(uri); + } else { + if (!isInvalidURI(uri)) { + cacheListener.cacheMissImageInfo(uri); + } + } + } + return info; + } + + /** + * Registers an ImageInfo instance with the cache. + * @param info the ImageInfo instance + */ + protected void putImageInfo(ImageInfo info) { + //An already existing ImageInfo is replaced. + imageInfos.put(info.getOriginalURI(), info); + } + + private static final long ONE_HOUR = 60 * 60 * 1000; + + /** + * Registers a URI as invalid so getImageInfo can indicate that quickly with no I/O access. + * @param uri the URI of the invalid image + */ + void registerInvalidURI(String uri) { + invalidURIs.put(uri, timeStampProvider.getTimeStamp()); + + considerHouseKeeping(); + } + + /** + * Returns an image from the cache or null if it wasn't found. + * @param info the ImageInfo instance representing the image + * @param flavor the requested ImageFlavor for the image + * @return the requested image or null if the image is not in the cache + */ + public Image getImage(ImageInfo info, ImageFlavor flavor) { + return getImage(info.getOriginalURI(), flavor); + } + + /** + * Returns an image from the cache or null if it wasn't found. + * @param uri the image's URI + * @param flavor the requested ImageFlavor for the image + * @return the requested image or null if the image is not in the cache + */ + public Image getImage(String uri, ImageFlavor flavor) { + if (uri == null || "".equals(uri)) { + return null; + } + ImageKey key = new ImageKey(uri, flavor); + Image img = (Image)images.get(key); + if (cacheListener != null) { + if (img != null) { + cacheListener.cacheHitImage(key); + } else { + cacheListener.cacheMissImage(key); + } + } + return img; + } + + /** + * Registers an image with the cache. + * @param img the image + */ + public void putImage(Image img) { + String originalURI = img.getInfo().getOriginalURI(); + if (originalURI == null || "".equals(originalURI)) { + return; //Don't cache if there's no URI + } + //An already existing Image is replaced. + if (!img.isCacheable()) { + throw new IllegalArgumentException( + "Image is not cacheable! (Flavor: " + img.getFlavor() + ")"); + } + ImageKey key = new ImageKey(originalURI, img.getFlavor()); + images.put(key, img); + } + + /** + * Clears the image cache (all ImageInfo and Image objects). + */ + public void clearCache() { + invalidURIs.clear(); + imageInfos.clear(); + images.clear(); + doHouseKeeping(); + } + + private void considerHouseKeeping() { + long ts = timeStampProvider.getTimeStamp(); + if (this.lastHouseKeeping + ONE_HOUR > ts) { + //Housekeeping is only triggered through registration of an invalid URI at the moment. + //Depending on the environment this could be triggered next to never. + //Doing this check for every image access could be relatively costly. + //The only alternative is a cleanup thread which is rather heavy-weight. + this.lastHouseKeeping = ts; + doHouseKeeping(); + } + } + + /** + * Triggers some house-keeping, i.e. removes stale entries. + */ + public void doHouseKeeping() { + imageInfos.doHouseKeeping(); + images.doHouseKeeping(); + doInvalidURIHouseKeeping(); + } + + private void doInvalidURIHouseKeeping() { + final Set currentEntries = new HashSet(this.invalidURIs.keySet()); + for (Object currentEntry : currentEntries) { + final String key = (String) currentEntry; + removeInvalidURIIfExpired(key); + } + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/cache/ImageCacheListener.java b/src/main/java/org/apache/xmlgraphics/image/loader/cache/ImageCacheListener.java new file mode 100644 index 0000000..6cd4c5d --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/cache/ImageCacheListener.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageCacheListener.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.image.loader.cache; + +import java.util.EventListener; + +/** + * This interface can be implemented by classes which want to know what's going on inside the + * image cache. + */ +public interface ImageCacheListener extends EventListener { + + /** + * An URi previously identified as invalid was requested again + * @param uri the invalid URI + */ + void invalidHit(String uri); + + /** + * An ImageInfo was found in the cache + * @param uri the image's URI + */ + void cacheHitImageInfo(String uri); + + /** + * An ImageInfo was not in the cache + * @param uri the image's URI + */ + void cacheMissImageInfo(String uri); + + /** + * An Image was found in the cache + * @param key the image key + */ + void cacheHitImage(ImageKey key); + + /** + * An Image was not in the cache + * @param key the image key + */ + void cacheMissImage(ImageKey key); + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/cache/ImageCacheStatistics.java b/src/main/java/org/apache/xmlgraphics/image/loader/cache/ImageCacheStatistics.java new file mode 100644 index 0000000..ada0edb --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/cache/ImageCacheStatistics.java @@ -0,0 +1,155 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageCacheStatistics.java 1681108 2015-05-22 13:26:12Z ssteiner $ */ + +package org.apache.xmlgraphics.image.loader.cache; + +import java.util.Collections; +import java.util.Map; + +/** + * Convenience class that gathers statistical information about the image cache. + */ +public class ImageCacheStatistics implements ImageCacheListener { + + private int invalidHits; + private int imageInfoCacheHits; + private int imageInfoCacheMisses; + private int imageCacheHits; + private int imageCacheMisses; + private Map imageCacheHitMap; + private Map imageCacheMissMap; + + /** + * Main constructor. + * @param detailed true if the cache hits/misses for each Image instance should be recorded. + */ + public ImageCacheStatistics(boolean detailed) { + if (detailed) { + imageCacheHitMap = new java.util.HashMap(); + imageCacheMissMap = new java.util.HashMap(); + } + } + + /** + * Reset the gathered statistics information. + */ + public void reset() { + this.imageInfoCacheHits = 0; + this.imageInfoCacheMisses = 0; + this.invalidHits = 0; + } + + /** {@inheritDoc} */ + public void invalidHit(String uri) { + invalidHits++; + } + + /** {@inheritDoc} */ + public void cacheHitImageInfo(String uri) { + imageInfoCacheHits++; + } + + /** {@inheritDoc} */ + public void cacheMissImageInfo(String uri) { + imageInfoCacheMisses++; + } + + private void increaseEntry(Map map, Object key) { + Integer v = (Integer)map.get(key); + if (v == null) { + v = 1; + } else { + v++; + } + map.put(key, v); + } + + /** {@inheritDoc} */ + public void cacheHitImage(ImageKey key) { + imageCacheHits++; + if (imageCacheHitMap != null) { + increaseEntry(imageCacheHitMap, key); + } + } + + /** {@inheritDoc} */ + public void cacheMissImage(ImageKey key) { + imageCacheMisses++; + if (imageCacheMissMap != null) { + increaseEntry(imageCacheMissMap, key); + } + } + + /** + * Returns the number of times an invalid URI is tried. + * @return the number of times an invalid URI is tried. + */ + public int getInvalidHits() { + return invalidHits; + } + + /** + * Returns the number of cache hits for ImageInfo instances. + * @return the number of cache hits for ImageInfo instances. + */ + public int getImageInfoCacheHits() { + return imageInfoCacheHits; + } + + /** + * Returns the number of cache misses for ImageInfo instances. + * @return the number of cache misses for ImageInfo instances. + */ + public int getImageInfoCacheMisses() { + return imageInfoCacheMisses; + } + + /** + * Returns the number of cache hits for Image instances. + * @return the number of cache hits for Image instances. + */ + public int getImageCacheHits() { + return imageCacheHits; + } + + /** + * Returns the number of cache misses for Image instances. + * @return the number of cache misses for Image instances. + */ + public int getImageCacheMisses() { + return imageCacheMisses; + } + + /** + * Returns a Map<ImageKey, Integer> with the number of cache hits. + * @return a Map<ImageKey, Integer> with the number of cache hits + */ + public Map getImageCacheHitMap() { + return Collections.unmodifiableMap(imageCacheHitMap); + } + + /** + * Returns a Map<ImageKey, Integer> with the number of cache misses. + * @return a Map<ImageKey, Integer> with the number of cache misses + */ + public Map getImageCacheMissMap() { + return Collections.unmodifiableMap(imageCacheMissMap); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/cache/ImageKey.java b/src/main/java/org/apache/xmlgraphics/image/loader/cache/ImageKey.java new file mode 100644 index 0000000..9c37850 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/cache/ImageKey.java @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageKey.java 1345683 2012-06-03 14:50:33Z gadams $ */ + +package org.apache.xmlgraphics.image.loader.cache; + +import org.apache.xmlgraphics.image.loader.ImageFlavor; + +/** + * Key class for Image instances in the cache. + */ +public class ImageKey { + + private String uri; + private org.apache.xmlgraphics.image.loader.ImageFlavor flavor; + + /** + * Main constructor. + * @param uri the original URI + * @param flavor the image flavor + */ + public ImageKey(String uri, ImageFlavor flavor) { + if (uri == null) { + throw new NullPointerException("URI must not be null"); + } + if (flavor == null) { + throw new NullPointerException("flavor must not be null"); + } + this.uri = uri; + this.flavor = flavor; + } + + /** {@inheritDoc} */ + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((flavor == null) ? 0 : flavor.hashCode()); + result = prime * result + ((uri == null) ? 0 : uri.hashCode()); + return result; + } + + /** {@inheritDoc} */ + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final ImageKey other = (ImageKey)obj; + if (!uri.equals(other.uri)) { + return false; + } + if (!flavor.equals(other.flavor)) { + return false; + } + return true; + } + + /** {@inheritDoc} */ + public String toString() { + return uri + " (" + flavor + ")"; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/cache/TimeStampProvider.java b/src/main/java/org/apache/xmlgraphics/image/loader/cache/TimeStampProvider.java new file mode 100644 index 0000000..583b505 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/cache/TimeStampProvider.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: TimeStampProvider.java 759150 2009-03-27 14:21:45Z jeremias $ */ + +package org.apache.xmlgraphics.image.loader.cache; + +/** + * Returns time stamps for the image cache for entry expiration functionality. This functionality + * is in its own class so it's easy to write a mock class for testing. + */ +class TimeStampProvider { + + /** + * Returns the current time stamp. + * @return the current time stamp (the value returned follows the semantics of + * {@link System#currentTimeMillis()}) + */ + public long getTimeStamp() { + return System.currentTimeMillis(); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/cache/package.html b/src/main/java/org/apache/xmlgraphics/image/loader/cache/package.html new file mode 100644 index 0000000..dfc7579 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/cache/package.html @@ -0,0 +1,25 @@ + + + +org.apache.fop.image2.cache Package + +

    + Contains image caching infrastructure. +

    + + \ No newline at end of file diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/impl/AbstractImage.java b/src/main/java/org/apache/xmlgraphics/image/loader/impl/AbstractImage.java new file mode 100644 index 0000000..d747799 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/impl/AbstractImage.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: AbstractImage.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.image.loader.impl; + +import java.awt.color.ColorSpace; +import java.awt.color.ICC_ColorSpace; +import java.awt.color.ICC_Profile; + +import org.apache.xmlgraphics.image.loader.Image; +import org.apache.xmlgraphics.image.loader.ImageInfo; +import org.apache.xmlgraphics.image.loader.ImageSize; + +/** + * Abstract base class for Image implementations. + */ +public abstract class AbstractImage implements Image { + + private ImageInfo info; + + /** + * Main constructor + * @param info the image info object associated with this image + */ + public AbstractImage(ImageInfo info) { + this.info = info; + } + + /** {@inheritDoc} */ + public ImageInfo getInfo() { + return this.info; + } + + /** {@inheritDoc} */ + public ImageSize getSize() { + return getInfo().getSize(); + } + + /** {@inheritDoc} */ + public ColorSpace getColorSpace() { + return null; + } + + /** {@inheritDoc} */ + public ICC_Profile getICCProfile() { + if (getColorSpace() instanceof ICC_ColorSpace) { + return ((ICC_ColorSpace)getColorSpace()).getProfile(); + } else { + return null; + } + } + + /** {@inheritDoc} */ + public String toString() { + return getClass().getName() + ": " + getInfo(); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/impl/AbstractImageConverter.java b/src/main/java/org/apache/xmlgraphics/image/loader/impl/AbstractImageConverter.java new file mode 100644 index 0000000..c363fbe --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/impl/AbstractImageConverter.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: AbstractImageConverter.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.image.loader.impl; + +import org.apache.xmlgraphics.image.loader.Image; +import org.apache.xmlgraphics.image.loader.spi.ImageConverter; + +/** + * Abstract base class for ImageConverter implementations. + */ +public abstract class AbstractImageConverter implements ImageConverter { + + /** + * Checks if the source flavor of the given image is compatible with this ImageConverter. + * @param img the image to check + */ + protected void checkSourceFlavor(Image img) { + if (!getSourceFlavor().equals(img.getFlavor())) { + throw new IllegalArgumentException("Incompatible image: " + img); + } + } + + /** {@inheritDoc} */ + public int getConversionPenalty() { + return MEDIUM_CONVERSION_PENALTY; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/impl/AbstractImageLoader.java b/src/main/java/org/apache/xmlgraphics/image/loader/impl/AbstractImageLoader.java new file mode 100644 index 0000000..03ba6df --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/impl/AbstractImageLoader.java @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: AbstractImageLoader.java 1681108 2015-05-22 13:26:12Z ssteiner $ */ + +package org.apache.xmlgraphics.image.loader.impl; + +import java.io.IOException; +import java.util.Map; + +import org.apache.xmlgraphics.image.loader.Image; +import org.apache.xmlgraphics.image.loader.ImageException; +import org.apache.xmlgraphics.image.loader.ImageInfo; +import org.apache.xmlgraphics.image.loader.ImageProcessingHints; +import org.apache.xmlgraphics.image.loader.ImageSessionContext; +import org.apache.xmlgraphics.image.loader.spi.ImageLoader; + +/** + * Simple abstract base class for ImageLoaders. + */ +public abstract class AbstractImageLoader implements ImageLoader { + + /** {@inheritDoc} */ + public Image loadImage(ImageInfo info, ImageSessionContext session) + throws ImageException, IOException { + return loadImage(info, null, session); + } + + /** {@inheritDoc} */ + public int getUsagePenalty() { + return NO_LOADING_PENALTY; + } + + /** + * Indicates whether an embedded color profile should be ignored. + * @param hints a Map of hints that can be used by implementations to customize the loading + * process (may be null). + * @return true if any color profile should be ignored + */ + protected boolean ignoreColorProfile(Map hints) { + if (hints == null) { + return false; + } + Boolean b = (Boolean)hints.get(ImageProcessingHints.IGNORE_COLOR_PROFILE); + return (b != null) && b; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/impl/AbstractImageLoaderFactory.java b/src/main/java/org/apache/xmlgraphics/image/loader/impl/AbstractImageLoaderFactory.java new file mode 100644 index 0000000..e4dce74 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/impl/AbstractImageLoaderFactory.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: AbstractImageLoaderFactory.java 924666 2010-03-18 08:26:30Z jeremias $ */ + +package org.apache.xmlgraphics.image.loader.impl; + +import org.apache.xmlgraphics.image.loader.ImageFlavor; +import org.apache.xmlgraphics.image.loader.ImageInfo; +import org.apache.xmlgraphics.image.loader.spi.ImageLoader; +import org.apache.xmlgraphics.image.loader.spi.ImageLoaderFactory; + +/** + * Abstract base class for ImageLoaderFactory implementations. + */ +public abstract class AbstractImageLoaderFactory implements ImageLoaderFactory { + + /** {@inheritDoc} */ + public boolean isSupported(ImageInfo imageInfo) { + //Most ImageLoaderFactories are assumed to support the complete feature set of + //an image format. + return true; + } + + /** + * {@inheritDoc} + * @deprecated Redundancy with {@link ImageLoader#getUsagePenalty()} + */ + public int getUsagePenalty(String mime, ImageFlavor flavor) { + //Kept for compatibility + ImageLoader loader = newImageLoader(flavor); + return loader.getUsagePenalty(); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/impl/AbstractImagePreloader.java b/src/main/java/org/apache/xmlgraphics/image/loader/impl/AbstractImagePreloader.java new file mode 100644 index 0000000..766bb58 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/impl/AbstractImagePreloader.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: AbstractImagePreloader.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.image.loader.impl; + +import java.io.IOException; +import java.util.Arrays; + +import javax.imageio.stream.ImageInputStream; + +import org.apache.xmlgraphics.image.loader.spi.ImagePreloader; + +/** + * Abstract base class for image preloaders. It provides helper methods for often-used tasks. + */ +public abstract class AbstractImagePreloader implements ImagePreloader { + + /** + * Allows to read an image header (usually just a magic number). The InputStream is reset + * to the position before the header was read. + * @param in the ImageInputStream to read from + * @param size the size of the header + * @return the loaded header + * @throws IOException if an I/O error occurs while reading the header + */ + protected byte[] getHeader(ImageInputStream in, int size) + throws IOException { + byte[] header = new byte[size]; + long startPos = in.getStreamPosition(); + int read = in.read(header); + if (read < size) { + Arrays.fill(header, (byte)0); + } + in.seek(startPos); //Seek back to start position + return header; + } + + /** {@inheritDoc} */ + public int getPriority() { + return DEFAULT_PRIORITY; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/impl/AbstractImageSessionContext.java b/src/main/java/org/apache/xmlgraphics/image/loader/impl/AbstractImageSessionContext.java new file mode 100644 index 0000000..033d5d3 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/impl/AbstractImageSessionContext.java @@ -0,0 +1,416 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: AbstractImageSessionContext.java 1895651 2021-12-07 08:03:10Z ssteiner $ */ + +package org.apache.xmlgraphics.image.loader.impl; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.AccessController; +import java.security.PrivilegedAction; + +import javax.imageio.ImageIO; +import javax.imageio.stream.ImageInputStream; +import javax.xml.transform.Source; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.sax.SAXSource; +import javax.xml.transform.stream.StreamSource; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.apache.xmlgraphics.image.loader.ImageSessionContext; +import org.apache.xmlgraphics.image.loader.ImageSource; +import org.apache.xmlgraphics.image.loader.util.ImageUtil; +import org.apache.xmlgraphics.image.loader.util.SoftMapCache; +import org.apache.xmlgraphics.io.XmlSourceUtil; + +/** + * Abstract base class for classes implementing ImageSessionContext. This class provides all the + * special treatment for Source creation, i.e. it provides optimized Source objects where possible. + */ +public abstract class AbstractImageSessionContext implements ImageSessionContext { + + /** logger */ + private static final Log log = LogFactory.getLog(AbstractImageSessionContext.class); + + private static boolean noSourceReuse; + + static { + noSourceReuse = AccessController.doPrivileged( + new PrivilegedAction() { + public Boolean run() { + //See: http://markmail.org/message/k6mno3jsxmovaz2e + String noSourceReuseString = System.getProperty( + AbstractImageSessionContext.class.getName() + ".no-source-reuse"); + return Boolean.valueOf(noSourceReuseString); + } + } + ); + } + + private final FallbackResolver fallbackResolver; + + public AbstractImageSessionContext() { + fallbackResolver = new UnrestrictedFallbackResolver(); + } + + /** + * @param fallbackResolver the fallback resolution mechanism to be used when simply getting an + * {@link InputStream} that backs a {@link Source} isn't possible. + */ + public AbstractImageSessionContext(FallbackResolver fallbackResolver) { + this.fallbackResolver = fallbackResolver; + } + + /** + * Attempts to resolve the given URI. + * @param uri URI to access + * @return A {@link javax.xml.transform.Source} object, or null if the URI + * cannot be resolved. + */ + protected abstract Source resolveURI(String uri); + + /** {@inheritDoc} */ + public Source newSource(String uri) { + Source source = resolveURI(uri); + if (source instanceof StreamSource || source instanceof SAXSource) { + return fallbackResolver.createSource(source, uri); + } + //Return any non-stream Sources and let the ImageLoaders deal with them + return source; + } + + protected static ImageInputStream createImageInputStream(InputStream in) throws IOException { + ImageInputStream iin = ImageIO.createImageInputStream(in); + return (ImageInputStream) Proxy.newProxyInstance( + ImageInputStream.class.getClassLoader(), + new Class[] {ImageInputStream.class}, + new ObservingImageInputStreamInvocationHandler(iin, in)); + } + + private static class ObservingImageInputStreamInvocationHandler + implements InvocationHandler { + + private ImageInputStream iin; + private InputStream in; + + public ObservingImageInputStreamInvocationHandler(ImageInputStream iin, + InputStream underlyingStream) { + this.iin = iin; + this.in = underlyingStream; + } + + /** {@inheritDoc} */ + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + try { + if ("close".equals(method.getName())) { + try { + return method.invoke(iin, args); + } finally { + IOUtils.closeQuietly(this.in); + this.in = null; + } + } else { + return method.invoke(iin, args); + } + } catch (InvocationTargetException e) { + throw e.getCause(); + } + } + + } + + /** + * Convert from a URL to a File. + *

    + * This method will decode the URL. + * Syntax such as file:///my%20docs/file.txt will be + * correctly decoded to /my docs/file.txt. + *

    + * Note: this method has been copied over from Apache Commons IO and enhanced to support + * UNC paths. + * + * @param url the file URL to convert, null returns null + * @return the equivalent File object, or null + * if the URL's protocol is not file + * @throws IllegalArgumentException if the file is incorrectly encoded + */ + public static File toFile(URL url) { + if (url == null || !url.getProtocol().equals("file")) { + return null; + } else { + try { + String filename = ""; + if (url.getHost() != null && url.getHost().length() > 0) { + filename += Character.toString(File.separatorChar) + + Character.toString(File.separatorChar) + + url.getHost(); + } + filename += url.getFile().replace('/', File.separatorChar); + filename = java.net.URLDecoder.decode(filename, "UTF-8"); + final File f = new File(filename); + if (!f.isFile()) { + return null; + } + return f; + } catch (java.io.UnsupportedEncodingException uee) { + assert false; + return null; + } + } + } + + private SoftMapCache sessionSources = new SoftMapCache(false); //no need for synchronization + + /** {@inheritDoc} */ + public Source getSource(String uri) { + return (Source) sessionSources.remove(uri); + } + + /** {@inheritDoc} */ + public Source needSource(String uri) throws FileNotFoundException { + Source src = getSource(uri); + if (src == null) { + if (log.isDebugEnabled()) { + log.debug("Creating new Source for " + uri); + + } + src = newSource(uri); + if (src == null) { + throw new FileNotFoundException("Image not found: " + uri); + } + } else { + if (log.isDebugEnabled()) { + log.debug("Reusing Source for " + uri); + } + } + return src; + } + + /** {@inheritDoc} */ + public void returnSource(String uri, Source src) { + //Safety check to make sure the Preloaders behave + ImageInputStream in = ImageUtil.getImageInputStream(src); + try { + if (in != null && in.getStreamPosition() != 0) { + throw new IllegalStateException("ImageInputStream is not reset for: " + uri); + } + } catch (IOException ioe) { + //Ignore exception + XmlSourceUtil.closeQuietly(src); + } + + if (isReusable(src)) { + //Only return the Source if it's reusable + log.debug("Returning Source for " + uri); + sessionSources.put(uri, src); + } else { + //Otherwise, try to close if possible and forget about it + XmlSourceUtil.closeQuietly(src); + } + } + + /** + * Indicates whether a Source is reusable. A Source object is reusable if it's an + * {@link ImageSource} (containing an {@link ImageInputStream}) or a {@link DOMSource}. + * @param src the Source object + * @return true if the Source is reusable + */ + protected boolean isReusable(Source src) { + if (noSourceReuse) { + return false; + } + if (src instanceof ImageSource) { + ImageSource is = (ImageSource) src; + if (is.getImageInputStream() != null) { + return true; + } + } + if (src instanceof DOMSource) { + return true; + } + return false; + } + + /** + * Implementations of this interface will be used as the mechanism for creating the + * {@link Source} that wraps resources. This interface allows clients to define their own + * implementations so that they have fine-grained control over how resources are acquired. + */ + public interface FallbackResolver { + + /** + * The fallback mechanism used to create the source which takes in both the {@link Source} + * that the the regular mechanisms attempted to create and the URI that the user provided. + * + * @param source the source + * @param uri the URI provided by the user + * @return the source that the contingency mechanism has been acquired + */ + Source createSource(Source source, String uri); + } + + /** + * An unrestricted resolver that has various contingency mechanisms that access the file-system. + * This is most suitable for use via the CLI or in environments where controlling I/O isn't a + * priority. + */ + public static final class UnrestrictedFallbackResolver implements FallbackResolver { + + /** {@inheritDoc} */ + public Source createSource(Source source, String uri) { + if (source == null) { + if (log.isDebugEnabled()) { + log.debug("URI could not be resolved: " + uri); + } + return null; + } + ImageSource imageSource = null; + + String resolvedURI = source.getSystemId(); + URL url; + try { + url = new URL(resolvedURI); + } catch (MalformedURLException e) { + url = null; + } + File f = /*FileUtils.*/toFile(url); + if (f != null) { + boolean directFileAccess = true; + assert (source instanceof StreamSource) || (source instanceof SAXSource); + InputStream in = XmlSourceUtil.getInputStream(source); + if (in == null) { + try { + in = new java.io.FileInputStream(f); + } catch (FileNotFoundException fnfe) { + log.error("Error while opening file." + + " Could not load image from system identifier '" + + source.getSystemId() + "' (" + fnfe.getMessage() + ")"); + return null; + } + } + in = ImageUtil.decorateMarkSupported(in); + try { + if (ImageUtil.isGZIPCompressed(in)) { + //GZIPped stream are not seekable, so buffer/cache like other URLs + directFileAccess = false; + } + } catch (IOException ioe) { + log.error("Error while checking the InputStream for GZIP compression." + + " Could not load image from system identifier '" + + source.getSystemId() + "' (" + ioe.getMessage() + ")"); + return null; + } + + if (directFileAccess) { + //Close as the file is reopened in a more optimal way + IOUtils.closeQuietly(in); + try { + // We let the OS' file system cache do the caching for us + // --> lower Java memory consumption, probably no speed loss + final ImageInputStream newInputStream = ImageIO + .createImageInputStream(f); + if (newInputStream == null) { + log.error("Unable to create ImageInputStream for local file " + + f + + " from system identifier '" + + source.getSystemId() + "'"); + return null; + } else { + imageSource = new ImageSource(newInputStream, + resolvedURI, true); + } + } catch (IOException ioe) { + log.error("Unable to create ImageInputStream for local file" + + " from system identifier '" + + source.getSystemId() + "' (" + ioe.getMessage() + ")"); + } + } + } + + if (imageSource == null) { + if (XmlSourceUtil.hasReader(source) && !ImageUtil.hasInputStream(source)) { + //We don't handle Reader instances here so return the Source unchanged + return source; + } + // Got a valid source, obtain an InputStream from it + InputStream in = XmlSourceUtil.getInputStream(source); + if (in == null && url != null) { + try { + in = url.openStream(); + } catch (Exception ex) { + log.error("Unable to obtain stream from system identifier '" + + source.getSystemId() + "'"); + } + } + if (in == null) { + log.error("The Source that was returned from URI resolution didn't contain" + + " an InputStream for URI: " + uri); + return null; + } + return createImageSource(in, source); + } + return imageSource; + } + } + + private static ImageSource createImageSource(InputStream in, Source source) { + try { + //Buffer and uncompress if necessary + return new ImageSource(createImageInputStream(ImageUtil.autoDecorateInputStream(in)), + source.getSystemId(), false); + } catch (IOException ioe) { + log.error("Unable to create ImageInputStream for InputStream" + + " from system identifier '" + + source.getSystemId() + "' (" + ioe.getMessage() + ")"); + } + return null; + } + + /** + * This fallback resolver is to be used in environments where controlling file access is of + * critical importance. It disallows any contingency mechanisms by which a {@link Source} object + * would be created. + */ + public static final class RestrictedFallbackResolver implements FallbackResolver { + + /** {@inheritDoc} */ + public Source createSource(Source source, String uri) { + if (source == null) { + if (log.isDebugEnabled()) { + log.debug("URI could not be resolved: " + uri); + } + return null; + } + if (ImageUtil.hasInputStream(source)) { + return createImageSource(XmlSourceUtil.getInputStream(source), source); + } + throw new UnsupportedOperationException("There are no contingency mechanisms for I/O."); + } + } +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/impl/CompositeImageLoader.java b/src/main/java/org/apache/xmlgraphics/image/loader/impl/CompositeImageLoader.java new file mode 100644 index 0000000..4f81963 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/impl/CompositeImageLoader.java @@ -0,0 +1,111 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: CompositeImageLoader.java 1804124 2017-08-04 14:13:54Z ssteiner $ */ + +package org.apache.xmlgraphics.image.loader.impl; + +import java.io.IOException; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.apache.xmlgraphics.image.loader.Image; +import org.apache.xmlgraphics.image.loader.ImageException; +import org.apache.xmlgraphics.image.loader.ImageFlavor; +import org.apache.xmlgraphics.image.loader.ImageInfo; +import org.apache.xmlgraphics.image.loader.ImageSessionContext; +import org.apache.xmlgraphics.image.loader.spi.ImageLoader; + +/** + * Composite ImageLoader implementation in order to provide fallbacks when one ImageLoader + * fails due to some limitation. + */ +public class CompositeImageLoader extends AbstractImageLoader { + + /** logger */ + protected static final Log log = LogFactory.getLog(CompositeImageLoader.class); + + private ImageLoader[] loaders; + + /** + * Main constructor. + * @param loaders the contained image loaders + */ + public CompositeImageLoader(ImageLoader[] loaders) { + if (loaders == null || loaders.length == 0) { + throw new IllegalArgumentException("Must at least pass one ImageLoader as parameter"); + } + for (int i = 1, c = loaders.length; i < c; i++) { + if (!loaders[0].getTargetFlavor().equals(loaders[i].getTargetFlavor())) { + throw new IllegalArgumentException( + "All ImageLoaders must produce the same target flavor"); + } + } + this.loaders = loaders; + } + + /** {@inheritDoc} */ + public ImageFlavor getTargetFlavor() { + return loaders[0].getTargetFlavor(); + } + + /** {@inheritDoc} */ + public int getUsagePenalty() { + int maxPenalty = NO_LOADING_PENALTY; + for (int i = 1, c = loaders.length; i < c; i++) { + maxPenalty = Math.max(maxPenalty, loaders[i].getUsagePenalty()); + } + return maxPenalty; + } + + /** {@inheritDoc} */ + public Image loadImage(ImageInfo info, Map hints, ImageSessionContext session) + throws ImageException, IOException { + ImageException firstException = null; + for (ImageLoader loader : this.loaders) { + try { + Image img = loader.loadImage(info, hints, session); + if (img != null && firstException != null) { + log.debug("First ImageLoader failed (" + firstException.getMessage() + + "). Fallback was successful."); + } + return img; + } catch (ImageException ie) { + if (firstException == null) { + firstException = ie; + } + } + } + throw firstException; + } + + /** {@inheritDoc} */ + public String toString() { + StringBuffer sb = new StringBuffer("["); + for (int i = 0; i < this.loaders.length; i++) { + if (i > 0) { + sb.append(","); + } + sb.append(this.loaders[i].toString()); + } + sb.append("]"); + return sb.toString(); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/impl/DefaultImageContext.java b/src/main/java/org/apache/xmlgraphics/image/loader/impl/DefaultImageContext.java new file mode 100644 index 0000000..a53a2de --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/impl/DefaultImageContext.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DefaultImageContext.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.image.loader.impl; + +import java.awt.GraphicsEnvironment; +import java.awt.Toolkit; + +import org.apache.xmlgraphics.image.GraphicsConstants; +import org.apache.xmlgraphics.image.loader.ImageContext; + +/** + * Very simple ImageContext implementation that uses the Toolkit's screen resolution. + */ +public class DefaultImageContext implements ImageContext { + + private final float sourceResolution; + + /** + * Main constructor. + */ + public DefaultImageContext() { + if (GraphicsEnvironment.isHeadless()) { + this.sourceResolution = GraphicsConstants.DEFAULT_DPI; + } else { + this.sourceResolution = Toolkit.getDefaultToolkit() + .getScreenResolution(); + } + } + + /** {@inheritDoc} */ + public float getSourceResolution() { + return this.sourceResolution; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/impl/DefaultImageSessionContext.java b/src/main/java/org/apache/xmlgraphics/image/loader/impl/DefaultImageSessionContext.java new file mode 100644 index 0000000..52e2231 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/impl/DefaultImageSessionContext.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DefaultImageSessionContext.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.image.loader.impl; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; + +import javax.xml.transform.Source; +import javax.xml.transform.stream.StreamSource; + +import org.apache.xmlgraphics.image.loader.ImageContext; + +/** + * Very simple implementation of the ImageSessionContext interface. It works for absolute URLs + * and local filenames only. Consider writing your own implementation of the ImageSessionContext + * if you need more sophisticated functionality. + */ +public class DefaultImageSessionContext extends AbstractImageSessionContext { + + private ImageContext context; + private File baseDir; + + /** + * Main constructor. + * @param context the parent image context + * @param baseDir the base directory for resolving relative filenames + */ + public DefaultImageSessionContext(ImageContext context, File baseDir) { + this.context = context; + this.baseDir = baseDir; + } + + /** {@inheritDoc} */ + public ImageContext getParentContext() { + return this.context; + } + + /** + * Returns the base directory for resolving relative filenames. + * @return the base directory + */ + public File getBaseDir() { + return this.baseDir; + } + + /** {@inheritDoc} */ + protected Source resolveURI(String uri) { + try { + URL url = new URL(uri); + return new StreamSource(url.openStream(), url.toExternalForm()); + } catch (MalformedURLException e) { + File f = new File(baseDir, uri); + if (f.isFile()) { + return new StreamSource(f); + } else { + return null; + } + } catch (IOException ioe) { + return null; + } + } + + /** {@inheritDoc} */ + public float getTargetResolution() { + return getParentContext().getSourceResolution(); //same as source resolution + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageBuffered.java b/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageBuffered.java new file mode 100644 index 0000000..3cc60a7 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageBuffered.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageBuffered.java 798806 2009-07-29 08:11:13Z maxberger $ */ + +package org.apache.xmlgraphics.image.loader.impl; + +import java.awt.Color; +import java.awt.image.BufferedImage; + +import org.apache.xmlgraphics.image.loader.ImageFlavor; +import org.apache.xmlgraphics.image.loader.ImageInfo; + +/** + * This class is an implementation of the Image interface exposing a BufferedImage. + */ +public class ImageBuffered extends ImageRendered { + + /** + * Main constructor. + * @param info the image info object + * @param buffered the BufferedImage instance + * @param transparentColor the transparent color or null + */ + public ImageBuffered(ImageInfo info, BufferedImage buffered, Color transparentColor) { + super(info, buffered, transparentColor); + } + + /** {@inheritDoc} */ + public ImageFlavor getFlavor() { + return ImageFlavor.BUFFERED_IMAGE; + } + + /** + * Returns the contained BufferedImage instance. + * @return the BufferedImage instance + */ + public java.awt.image.BufferedImage getBufferedImage() { + return (BufferedImage)getRenderedImage(); + } +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageConverterBitmap2G2D.java b/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageConverterBitmap2G2D.java new file mode 100644 index 0000000..f4f0866 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageConverterBitmap2G2D.java @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageConverterBitmap2G2D.java 1681108 2015-05-22 13:26:12Z ssteiner $ */ + +package org.apache.xmlgraphics.image.loader.impl; + +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.geom.AffineTransform; +import java.awt.geom.Rectangle2D; +import java.awt.image.RenderedImage; +import java.util.Map; + +import org.apache.xmlgraphics.image.loader.Image; +import org.apache.xmlgraphics.image.loader.ImageFlavor; +import org.apache.xmlgraphics.java2d.Graphics2DImagePainter; + +/** + * This ImageConverter wraps a bitmap image in a Graphics2D image. + */ +public class ImageConverterBitmap2G2D extends AbstractImageConverter { + + /** {@inheritDoc} */ + public Image convert(Image src, Map hints) { + checkSourceFlavor(src); + assert src instanceof ImageRendered; + final ImageRendered rendImage = (ImageRendered)src; + + Graphics2DImagePainterImpl painter = new Graphics2DImagePainterImpl(rendImage); + ImageGraphics2D g2dImage = new ImageGraphics2D(src.getInfo(), painter); + return g2dImage; + } + + static class Graphics2DImagePainterImpl implements Graphics2DImagePainter { + ImageRendered rendImage; + public Graphics2DImagePainterImpl(ImageRendered rendImage) { + this.rendImage = rendImage; + } + public Dimension getImageSize() { + return rendImage.getSize().getDimensionMpt(); + } + public void paint(Graphics2D g2d, Rectangle2D area) { + RenderedImage ri = rendImage.getRenderedImage(); + double w = area.getWidth(); + double h = area.getHeight(); + + AffineTransform at = new AffineTransform(); + at.translate(area.getX(), area.getY()); + //Scale image to fit + double sx = w / ri.getWidth(); + double sy = h / ri.getHeight(); + if (sx != 1.0 || sy != 1.0) { + at.scale(sx, sy); + } + g2d.drawRenderedImage(ri, at); + } + } + + /** {@inheritDoc} */ + public ImageFlavor getSourceFlavor() { + return ImageFlavor.RENDERED_IMAGE; + } + + /** {@inheritDoc} */ + public ImageFlavor getTargetFlavor() { + return ImageFlavor.GRAPHICS2D; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageConverterBuffered2Rendered.java b/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageConverterBuffered2Rendered.java new file mode 100644 index 0000000..76d0daf --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageConverterBuffered2Rendered.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageConverterBuffered2Rendered.java 1681108 2015-05-22 13:26:12Z ssteiner $ */ + +package org.apache.xmlgraphics.image.loader.impl; + +import java.util.Map; + +import org.apache.xmlgraphics.image.loader.Image; +import org.apache.xmlgraphics.image.loader.ImageFlavor; + +/** + * This ImageConverter converts BufferedImages to RenderedImages (well, it's basically just a + * class cast). + */ +public class ImageConverterBuffered2Rendered extends AbstractImageConverter { + + /** {@inheritDoc} */ + public Image convert(Image src, Map hints) { + checkSourceFlavor(src); + assert src instanceof ImageBuffered; + ImageBuffered buffered = (ImageBuffered)src; + return new ImageRendered(buffered.getInfo(), buffered + .getRenderedImage(), buffered.getTransparentColor()); + } + + /** {@inheritDoc} */ + public ImageFlavor getSourceFlavor() { + return ImageFlavor.BUFFERED_IMAGE; + } + + /** {@inheritDoc} */ + public ImageFlavor getTargetFlavor() { + return ImageFlavor.RENDERED_IMAGE; + } + + /** {@inheritDoc} */ + public int getConversionPenalty() { + return NO_CONVERSION_PENALTY; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageConverterG2D2Bitmap.java b/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageConverterG2D2Bitmap.java new file mode 100644 index 0000000..57a3360 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageConverterG2D2Bitmap.java @@ -0,0 +1,214 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageConverterG2D2Bitmap.java 1754511 2016-07-29 13:09:37Z ssteiner $ */ + +package org.apache.xmlgraphics.image.loader.impl; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.RenderingHints; +import java.awt.Transparency; +import java.awt.color.ColorSpace; +import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; +import java.awt.image.ColorModel; +import java.awt.image.ComponentColorModel; +import java.awt.image.DataBuffer; +import java.awt.image.PixelInterleavedSampleModel; +import java.awt.image.Raster; +import java.awt.image.WritableRaster; +import java.util.Map; + +import org.apache.xmlgraphics.image.GraphicsConstants; +import org.apache.xmlgraphics.image.loader.Image; +import org.apache.xmlgraphics.image.loader.ImageFlavor; +import org.apache.xmlgraphics.image.loader.ImageProcessingHints; +import org.apache.xmlgraphics.image.loader.ImageSize; +import org.apache.xmlgraphics.java2d.color.DeviceCMYKColorSpace; +import org.apache.xmlgraphics.util.UnitConv; + +/** + * This ImageConverter converts Graphics2D images to a BufferedImage. + */ +public class ImageConverterG2D2Bitmap extends AbstractImageConverter { + + /** {@inheritDoc} */ + public Image convert(Image src, Map hints) { + checkSourceFlavor(src); + assert src instanceof ImageGraphics2D; + ImageGraphics2D g2dImage = (ImageGraphics2D)src; + + Object formatIntent = hints.get(ImageProcessingHints.BITMAP_TYPE_INTENT); + int bitsPerPixel = 24; + if (ImageProcessingHints.BITMAP_TYPE_INTENT_GRAY.equals(formatIntent)) { + bitsPerPixel = 8; + } else if (ImageProcessingHints.BITMAP_TYPE_INTENT_MONO.equals(formatIntent)) { + bitsPerPixel = 1; + } + + Object transparencyIntent = hints.get(ImageProcessingHints.TRANSPARENCY_INTENT); + boolean withAlpha = true; + if (ImageProcessingHints.TRANSPARENCY_INTENT_IGNORE.equals(transparencyIntent)) { + withAlpha = false; + } + + int resolution = GraphicsConstants.DEFAULT_SAMPLE_DPI; + Number res = (Number)hints.get(ImageProcessingHints.TARGET_RESOLUTION); + if (res != null) { + resolution = res.intValue(); + } + boolean cmyk = Boolean.TRUE.equals(hints.get("CMYK")); + + BufferedImage bi = paintToBufferedImage(g2dImage, bitsPerPixel, withAlpha, resolution, cmyk); + + ImageBuffered bufImage = new ImageBuffered(src.getInfo(), bi, null); + return bufImage; + } + + /** + * Paints a Graphics2D image on a BufferedImage and returns this bitmap. + * @param g2dImage the Graphics2D image + * @param bitsPerPixel the desired number of bits per pixel (supported: 1, 8, 24) + * @param withAlpha true if the generated image should have an alpha channel + * @param resolution the requested bitmap resolution + * @return the newly created BufferedImage + */ + protected BufferedImage paintToBufferedImage(ImageGraphics2D g2dImage, + int bitsPerPixel, boolean withAlpha, int resolution, boolean cmyk) { + ImageSize size = g2dImage.getSize(); + + RenderingHints additionalHints = null; + int bmw = (int)Math.ceil(UnitConv.mpt2px(size.getWidthMpt(), resolution)); + int bmh = (int)Math.ceil(UnitConv.mpt2px(size.getHeightMpt(), resolution)); + BufferedImage bi; + switch (bitsPerPixel) { + case 1: + bi = new BufferedImage(bmw, bmh, BufferedImage.TYPE_BYTE_BINARY); + withAlpha = false; + //withAlpha is ignored in this case + additionalHints = new RenderingHints(null); + //The following usually has no effect but some class libraries might support it + additionalHints.put(RenderingHints.KEY_DITHERING, + RenderingHints.VALUE_DITHER_ENABLE); + break; + case 8: + if (withAlpha) { + bi = createGrayBufferedImageWithAlpha(bmw, bmh); + } else { + bi = new BufferedImage(bmw, bmh, BufferedImage.TYPE_BYTE_GRAY); + } + break; + default: + if (withAlpha) { + bi = new BufferedImage(bmw, bmh, BufferedImage.TYPE_INT_ARGB); + } else { + if (cmyk) { + ComponentColorModel ccm = new ComponentColorModel( + new DeviceCMYKColorSpace(), false, false, 1, DataBuffer.TYPE_BYTE); + int[] bands = {0, 1, 2, 3}; + PixelInterleavedSampleModel sm = new PixelInterleavedSampleModel( + DataBuffer.TYPE_BYTE, bmw, bmh, 4, bmw * 4, bands); + WritableRaster raster = WritableRaster.createWritableRaster(sm, new Point(0, 0)); + bi = new BufferedImage(ccm, raster, false, null); + } else { + bi = new BufferedImage(bmw, bmh, BufferedImage.TYPE_INT_RGB); + } + } + } + + Graphics2D g2d = bi.createGraphics(); + try { + g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, + RenderingHints.VALUE_FRACTIONALMETRICS_ON); + setRenderingHintsForBufferedImage(g2d); + if (additionalHints != null) { + g2d.addRenderingHints(additionalHints); + } + + g2d.setBackground(Color.white); + g2d.setColor(Color.black); + if (!withAlpha) { + g2d.clearRect(0, 0, bmw, bmh); + } + /* debug code + int off = 2; + g2d.drawLine(off, 0, off, bmh); + g2d.drawLine(bmw - off, 0, bmw - off, bmh); + g2d.drawLine(0, off, bmw, off); + g2d.drawLine(0, bmh - off, bmw, bmh - off); + */ + double sx = (double)bmw / size.getWidthMpt(); + double sy = (double)bmh / size.getHeightMpt(); + g2d.scale(sx, sy); + + //Paint the image on the BufferedImage + Rectangle2D area = new Rectangle2D.Double( + 0.0, 0.0, size.getWidthMpt(), size.getHeightMpt()); + g2dImage.getGraphics2DImagePainter().paint(g2d, area); + } finally { + g2d.dispose(); + } + return bi; + } + + private static BufferedImage createGrayBufferedImageWithAlpha(int width, int height) { + BufferedImage bi; + boolean alphaPremultiplied = true; + int bands = 2; + int[] bits = new int[bands]; + for (int i = 0; i < bands; i++) { + bits[i] = 8; + } + ColorModel cm = new ComponentColorModel( + ColorSpace.getInstance(ColorSpace.CS_GRAY), + bits, + true, alphaPremultiplied, + Transparency.TRANSLUCENT, + DataBuffer.TYPE_BYTE); + WritableRaster wr = Raster.createInterleavedRaster( + DataBuffer.TYPE_BYTE, + width, height, bands, + new Point(0, 0)); + bi = new BufferedImage(cm, wr, alphaPremultiplied, null); + return bi; + } + + /** + * Sets rendering hints on the Graphics2D created for painting to a BufferedImage. Subclasses + * can modify the settings to customize the behaviour. + * @param g2d the Graphics2D instance + */ + protected void setRenderingHintsForBufferedImage(Graphics2D g2d) { + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_OFF); + g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, + RenderingHints.VALUE_TEXT_ANTIALIAS_OFF); + } + + /** {@inheritDoc} */ + public ImageFlavor getSourceFlavor() { + return ImageFlavor.GRAPHICS2D; + } + + /** {@inheritDoc} */ + public ImageFlavor getTargetFlavor() { + return ImageFlavor.BUFFERED_IMAGE; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageConverterRendered2PNG.java b/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageConverterRendered2PNG.java new file mode 100644 index 0000000..01686b6 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageConverterRendered2PNG.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageConverterRendered2PNG.java 1681108 2015-05-22 13:26:12Z ssteiner $ */ + +package org.apache.xmlgraphics.image.loader.impl; + +import java.io.IOException; +import java.util.Map; + +import org.apache.commons.io.output.ByteArrayOutputStream; + +import org.apache.xmlgraphics.image.loader.Image; +import org.apache.xmlgraphics.image.loader.ImageException; +import org.apache.xmlgraphics.image.loader.ImageFlavor; +import org.apache.xmlgraphics.image.writer.ImageWriter; +import org.apache.xmlgraphics.image.writer.ImageWriterParams; +import org.apache.xmlgraphics.image.writer.ImageWriterRegistry; +import org.apache.xmlgraphics.util.MimeConstants; + +/** + * This ImageConverter converts Rendered to PNG images. + */ +public class ImageConverterRendered2PNG extends AbstractImageConverter { + + /** {@inheritDoc} */ + public Image convert(Image src, Map hints) throws ImageException, IOException { + checkSourceFlavor(src); + assert src instanceof ImageRendered; + ImageRendered rendered = (ImageRendered)src; + ImageWriter writer = ImageWriterRegistry.getInstance().getWriterFor(MimeConstants.MIME_PNG); + if (writer == null) { + throw new ImageException("Cannot convert image to PNG. No suitable ImageWriter found."); + } + ByteArrayOutputStream baout = new ByteArrayOutputStream(); + ImageWriterParams params = new ImageWriterParams(); + params.setResolution((int)Math.round(src.getSize().getDpiHorizontal())); + writer.writeImage(rendered.getRenderedImage(), baout, params); + return new ImageRawStream(src.getInfo(), getTargetFlavor(), + new java.io.ByteArrayInputStream(baout.toByteArray())); + } + + /** {@inheritDoc} */ + public ImageFlavor getSourceFlavor() { + return ImageFlavor.RENDERED_IMAGE; + } + + /** {@inheritDoc} */ + public ImageFlavor getTargetFlavor() { + return ImageFlavor.RAW_PNG; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageGraphics2D.java b/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageGraphics2D.java new file mode 100644 index 0000000..0fdb880 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageGraphics2D.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageGraphics2D.java 1784728 2017-02-28 11:53:12Z ssteiner $ */ + +package org.apache.xmlgraphics.image.loader.impl; + +import org.apache.xmlgraphics.image.loader.Image; +import org.apache.xmlgraphics.image.loader.ImageFlavor; +import org.apache.xmlgraphics.image.loader.ImageInfo; +import org.apache.xmlgraphics.java2d.Graphics2DImagePainter; + +/** + * This class is an implementation of the Image interface exposing a Graphics2DImagePainter. + */ +public class ImageGraphics2D extends AbstractImage { + + private Graphics2DImagePainter painter; + + /** + * Main constructor. + * @param info the image info object + * @param painter the image painter that will paint the Java2D image + */ + public ImageGraphics2D(ImageInfo info, Graphics2DImagePainter painter) { + super(info); + setGraphics2DImagePainter(painter); + } + + /** {@inheritDoc} */ + public ImageFlavor getFlavor() { + return ImageFlavor.GRAPHICS2D; + } + + /** {@inheritDoc} */ + public boolean isCacheable() { + Image img = getInfo().getOriginalImage(); + if (img == null) { + return true; + } + return img.isCacheable(); + } + + /** + * Returns the contained Graphics2DImagePainter instance. + * @return the image painter + */ + public Graphics2DImagePainter getGraphics2DImagePainter() { + return this.painter; + } + + /** + * Sets the Graphics2DImagePainter instance. + * @param painter the image painter + */ + public void setGraphics2DImagePainter(Graphics2DImagePainter painter) { + this.painter = painter; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageLoaderEPS.java b/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageLoaderEPS.java new file mode 100644 index 0000000..25ee8b0 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageLoaderEPS.java @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageLoaderEPS.java 1391005 2012-09-27 13:39:57Z mehdi $ */ + +package org.apache.xmlgraphics.image.loader.impl; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; + +import javax.xml.transform.Source; + +import org.apache.xmlgraphics.image.loader.Image; +import org.apache.xmlgraphics.image.loader.ImageException; +import org.apache.xmlgraphics.image.loader.ImageFlavor; +import org.apache.xmlgraphics.image.loader.ImageInfo; +import org.apache.xmlgraphics.image.loader.ImageSessionContext; +import org.apache.xmlgraphics.io.XmlSourceUtil; +import org.apache.xmlgraphics.util.MimeConstants; +import org.apache.xmlgraphics.util.io.SubInputStream; + +/** + * ImageLoader for EPS (Encapsulated PostScript) images. + */ +public class ImageLoaderEPS extends AbstractImageLoader { + + /** + * Main constructor. + */ + public ImageLoaderEPS() { + } + + /** {@inheritDoc} */ + public ImageFlavor getTargetFlavor() { + return ImageFlavor.RAW_EPS; + } + + /** {@inheritDoc} */ + public Image loadImage(ImageInfo info, Map hints, ImageSessionContext session) + throws ImageException, IOException { + if (!MimeConstants.MIME_EPS.equals(info.getMimeType())) { + throw new IllegalArgumentException( + "ImageInfo must be from a image with MIME type: " + MimeConstants.MIME_EPS); + } + Source src = session.needSource(info.getOriginalURI()); + InputStream in = XmlSourceUtil.needInputStream(src); + XmlSourceUtil.removeStreams(src); //so others cannot close them, we take them over + + PreloaderEPS.EPSBinaryFileHeader binaryHeader; + binaryHeader = (PreloaderEPS.EPSBinaryFileHeader)info.getCustomObjects().get( + PreloaderEPS.EPS_BINARY_HEADER); + if (binaryHeader != null) { + //Binary EPS: just extract the EPS part + in.skip(binaryHeader.getPSStart()); + in = new SubInputStream(in, binaryHeader.getPSLength(), true); + } + + ImageRawEPS epsImage = new ImageRawEPS(info, in); + return epsImage; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageLoaderFactoryEPS.java b/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageLoaderFactoryEPS.java new file mode 100644 index 0000000..05348cd --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageLoaderFactoryEPS.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageLoaderFactoryEPS.java 1681137 2015-05-22 14:54:05Z ssteiner $ */ + +package org.apache.xmlgraphics.image.loader.impl; + +import org.apache.xmlgraphics.image.loader.ImageFlavor; +import org.apache.xmlgraphics.image.loader.spi.ImageLoader; +import org.apache.xmlgraphics.util.MimeConstants; + +/** + * Factory class for the ImageLoader for EPS (Encapsulated PostScript) images. + */ +public class ImageLoaderFactoryEPS extends AbstractImageLoaderFactory { + + private static final String[] MIMES = new String[] { + MimeConstants.MIME_EPS}; + + private static final ImageFlavor[] FLAVORS = new ImageFlavor[] { + ImageFlavor.RAW_EPS}; + + + /** {@inheritDoc} */ + public String[] getSupportedMIMETypes() { + return MIMES; + } + + /** {@inheritDoc} */ + public ImageFlavor[] getSupportedFlavors(String mime) { + if (MimeConstants.MIME_EPS.equals(mime)) { + return FLAVORS; + } + throw new IllegalArgumentException("Unsupported MIME type: " + mime); + } + + /** {@inheritDoc} */ + public ImageLoader newImageLoader(ImageFlavor targetFlavor) { + return new ImageLoaderEPS(); + } + + /** {@inheritDoc} */ + public boolean isAvailable() { + return true; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageLoaderFactoryInternalTIFF.java b/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageLoaderFactoryInternalTIFF.java new file mode 100644 index 0000000..da3f84a --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageLoaderFactoryInternalTIFF.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageLoaderFactoryInternalTIFF.java 1681137 2015-05-22 14:54:05Z ssteiner $ */ + +package org.apache.xmlgraphics.image.loader.impl; + +import org.apache.xmlgraphics.image.loader.ImageFlavor; +import org.apache.xmlgraphics.image.loader.spi.ImageLoader; +import org.apache.xmlgraphics.util.MimeConstants; + +/** + * Factory class for the ImageLoader for TIFF (based on Commons' internal TIFF codec). + */ +public class ImageLoaderFactoryInternalTIFF extends AbstractImageLoaderFactory { + + private static final String[] MIMES = new String[] { + MimeConstants.MIME_TIFF}; + + private static final ImageFlavor[] FLAVORS = new ImageFlavor[] { + ImageFlavor.RENDERED_IMAGE}; + + + /** {@inheritDoc} */ + public String[] getSupportedMIMETypes() { + return MIMES; + } + + /** {@inheritDoc} */ + public ImageFlavor[] getSupportedFlavors(String mime) { + if (MimeConstants.MIME_TIFF.equals(mime)) { + return FLAVORS; + } + throw new IllegalArgumentException("Unsupported MIME type: " + mime); + } + + /** {@inheritDoc} */ + public ImageLoader newImageLoader(ImageFlavor targetFlavor) { + return new ImageLoaderInternalTIFF(); + } + + /** {@inheritDoc} */ + public boolean isAvailable() { + return true; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageLoaderFactoryPNG.java b/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageLoaderFactoryPNG.java new file mode 100644 index 0000000..9147c25 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageLoaderFactoryPNG.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageLoaderFactoryPNG.java 1732019 2016-02-24 05:01:10Z gadams $ */ + +package org.apache.xmlgraphics.image.loader.impl; + +import org.apache.xmlgraphics.image.loader.ImageFlavor; +import org.apache.xmlgraphics.image.loader.spi.ImageLoader; +import org.apache.xmlgraphics.util.MimeConstants; + +public class ImageLoaderFactoryPNG extends AbstractImageLoaderFactory { + + private static final String[] MIMES = new String[] {MimeConstants.MIME_PNG}; + + private static final ImageFlavor[] FLAVORS = new ImageFlavor[] {ImageFlavor.RENDERED_IMAGE}; + + public ImageLoaderFactoryPNG() { + // + } + + /** {@inheritDoc} */ + public String[] getSupportedMIMETypes() { + return MIMES; + } + + /** {@inheritDoc} */ + public ImageFlavor[] getSupportedFlavors(String mime) { + if (MimeConstants.MIME_PNG.equals(mime)) { + return FLAVORS; + } + throw new IllegalArgumentException("Unsupported MIME type: " + mime); + } + + /** {@inheritDoc} */ + public ImageLoader newImageLoader(ImageFlavor targetFlavor) { + return new ImageLoaderPNG(); + } + + /** {@inheritDoc} */ + public boolean isAvailable() { + return true; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageLoaderFactoryRaw.java b/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageLoaderFactoryRaw.java new file mode 100644 index 0000000..ae8a9c1 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageLoaderFactoryRaw.java @@ -0,0 +1,96 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageLoaderFactoryRaw.java 1681137 2015-05-22 14:54:05Z ssteiner $ */ + +package org.apache.xmlgraphics.image.loader.impl; + +import org.apache.xmlgraphics.image.loader.ImageFlavor; +import org.apache.xmlgraphics.image.loader.spi.ImageLoader; +import org.apache.xmlgraphics.util.MimeConstants; + +/** + * Factory class for the ImageLoader for raw/undecoded images. + */ +public class ImageLoaderFactoryRaw extends AbstractImageLoaderFactory { + + /** MIME type for EMF (Windows Enhanced Metafile) */ + public static final String MIME_EMF = "image/x-emf"; + + private static final String[] MIMES = new String[] { + MimeConstants.MIME_PNG, + MimeConstants.MIME_JPEG, + MimeConstants.MIME_TIFF, + MIME_EMF}; + + private static final ImageFlavor[][] FLAVORS = new ImageFlavor[][] { + {ImageFlavor.RAW_PNG}, + {ImageFlavor.RAW_JPEG}, + {ImageFlavor.RAW_TIFF}, + {ImageFlavor.RAW_EMF}}; + + + /** + * Returns the MIME type for a given ImageFlavor if it is from a format that is consumed + * without being undecoded. If the ImageFlavor is no raw flavor, an IllegalArgumentException + * is thrown. + * @param flavor the image flavor + * @return the associated MIME type + */ + public static String getMimeForRawFlavor(ImageFlavor flavor) { + for (int i = 0, ci = FLAVORS.length; i < ci; i++) { + for (int j = 0, cj = FLAVORS[i].length; j < cj; j++) { + if (FLAVORS[i][j].equals(flavor)) { + return MIMES[i]; + } + } + } + throw new IllegalArgumentException("ImageFlavor is not a \"raw\" flavor: " + flavor); + } + + /** {@inheritDoc} */ + public String[] getSupportedMIMETypes() { + return MIMES; + } + + /** {@inheritDoc} */ + public ImageFlavor[] getSupportedFlavors(String mime) { + for (int i = 0, c = MIMES.length; i < c; i++) { + if (MIMES[i].equals(mime)) { + return FLAVORS[i]; + } + } + throw new IllegalArgumentException("Unsupported MIME type: " + mime); + } + + /** {@inheritDoc} */ + public ImageLoader newImageLoader(ImageFlavor targetFlavor) { + if (targetFlavor.equals(ImageFlavor.RAW_JPEG)) { + return new ImageLoaderRawJPEG(); + } else if (targetFlavor.equals(ImageFlavor.RAW_PNG)) { + return new ImageLoaderRawPNG(); + } else { + return new ImageLoaderRaw(targetFlavor); + } + } + + /** {@inheritDoc} */ + public boolean isAvailable() { + return true; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageLoaderFactoryRawCCITTFax.java b/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageLoaderFactoryRawCCITTFax.java new file mode 100644 index 0000000..cb71528 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageLoaderFactoryRawCCITTFax.java @@ -0,0 +1,120 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageLoaderFactoryRawCCITTFax.java 1681137 2015-05-22 14:54:05Z ssteiner $ */ + +package org.apache.xmlgraphics.image.loader.impl; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.apache.xmlgraphics.image.codec.tiff.TIFFImage; +import org.apache.xmlgraphics.image.loader.ImageFlavor; +import org.apache.xmlgraphics.image.loader.ImageInfo; +import org.apache.xmlgraphics.image.loader.spi.ImageLoader; +import org.apache.xmlgraphics.util.MimeConstants; + +/** + * Factory class for the ImageLoader for raw/undecoded CCITT encoded images. + */ +public class ImageLoaderFactoryRawCCITTFax extends AbstractImageLoaderFactory { + + /** logger */ + private transient Log log = LogFactory.getLog(ImageLoaderFactoryRawCCITTFax.class); + + private static final String[] MIMES = new String[] { + MimeConstants.MIME_TIFF}; + + private static final ImageFlavor[][] FLAVORS = new ImageFlavor[][] { + {ImageFlavor.RAW_CCITTFAX}}; + + + /** + * Returns the MIME type for a given ImageFlavor if it is from a format that is consumed + * without being undecoded. If the ImageFlavor is no raw flavor, an IllegalArgumentException + * is thrown. + * @param flavor the image flavor + * @return the associated MIME type + */ + public static String getMimeForRawFlavor(ImageFlavor flavor) { + for (int i = 0, ci = FLAVORS.length; i < ci; i++) { + for (int j = 0, cj = FLAVORS[i].length; j < cj; j++) { + if (FLAVORS[i][j].equals(flavor)) { + return MIMES[i]; + } + } + } + throw new IllegalArgumentException("ImageFlavor is not a \"raw\" flavor: " + flavor); + } + + /** {@inheritDoc} */ + public String[] getSupportedMIMETypes() { + return MIMES; + } + + /** {@inheritDoc} */ + public ImageFlavor[] getSupportedFlavors(String mime) { + for (int i = 0, c = MIMES.length; i < c; i++) { + if (MIMES[i].equals(mime)) { + return FLAVORS[i]; + } + } + throw new IllegalArgumentException("Unsupported MIME type: " + mime); + } + + /** {@inheritDoc} */ + public ImageLoader newImageLoader(ImageFlavor targetFlavor) { + if (targetFlavor.equals(ImageFlavor.RAW_CCITTFAX)) { + return new ImageLoaderRawCCITTFax(); + } else { + throw new IllegalArgumentException("Unsupported image flavor: " + targetFlavor); + } + } + + /** {@inheritDoc} */ + public boolean isAvailable() { + return true; + } + + /** {@inheritDoc} */ + public boolean isSupported(ImageInfo imageInfo) { + Boolean tiled = (Boolean)imageInfo.getCustomObjects().get("TIFF_TILED"); + if (Boolean.TRUE.equals(tiled)) { + //We don't support tiled images + log.trace("Raw CCITT loading not supported for tiled TIFF image"); + return false; + } + Integer compression = (Integer)imageInfo.getCustomObjects().get("TIFF_COMPRESSION"); + if (compression == null) { + return false; + } + switch (compression) { + case TIFFImage.COMP_FAX_G3_1D: + case TIFFImage.COMP_FAX_G3_2D: + case TIFFImage.COMP_FAX_G4_2D: + Integer stripCount = (Integer)imageInfo.getCustomObjects().get("TIFF_STRIP_COUNT"); + boolean supported = (stripCount != null && stripCount == 1); + if (!supported) { + log.trace("Raw CCITT loading not supported for multi-strip TIFF image"); + } + return supported; + default: + return false; + } + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageLoaderInternalTIFF.java b/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageLoaderInternalTIFF.java new file mode 100644 index 0000000..a4eee11 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageLoaderInternalTIFF.java @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageLoaderInternalTIFF.java 1681108 2015-05-22 13:26:12Z ssteiner $ */ + +package org.apache.xmlgraphics.image.loader.impl; + +import java.io.IOException; +import java.util.Map; + +import javax.imageio.stream.ImageInputStream; +import javax.xml.transform.Source; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.apache.xmlgraphics.image.codec.util.ImageInputStreamSeekableStreamAdapter; +import org.apache.xmlgraphics.image.codec.util.SeekableStream; +import org.apache.xmlgraphics.image.loader.Image; +import org.apache.xmlgraphics.image.loader.ImageException; +import org.apache.xmlgraphics.image.loader.ImageFlavor; +import org.apache.xmlgraphics.image.loader.ImageInfo; +import org.apache.xmlgraphics.image.loader.ImageSessionContext; +import org.apache.xmlgraphics.image.loader.util.ImageUtil; + +/** + * An ImageLoader implementation based on Commons' internal TIFF codec. + */ +public class ImageLoaderInternalTIFF extends AbstractImageLoader { + + /** logger */ + protected static final Log log = LogFactory.getLog(ImageLoaderInternalTIFF.class); + + /** + * Main constructor. + */ + public ImageLoaderInternalTIFF() { + //nop + } + + /** {@inheritDoc} */ + public ImageFlavor getTargetFlavor() { + return ImageFlavor.RENDERED_IMAGE; + } + + /** {@inheritDoc} */ + public Image loadImage(ImageInfo info, Map hints, ImageSessionContext session) + throws ImageException, IOException { + + Source src = session.needSource(info.getOriginalURI()); + ImageInputStream imgStream = ImageUtil.needImageInputStream(src); + + SeekableStream seekStream = new ImageInputStreamSeekableStreamAdapter(imgStream); + try { + org.apache.xmlgraphics.image.codec.tiff.TIFFImage img + = new org.apache.xmlgraphics.image.codec.tiff.TIFFImage( + seekStream, null, 0); + // TODO: This may ignore ICC Profiles stored in TIFF images. + return new ImageRendered(info, img, null); + } catch (RuntimeException e) { + throw new ImageException("Could not load image with internal TIFF codec", e); + } + } + + /** {@inheritDoc} */ + public int getUsagePenalty() { + return 1000; //Provide this only as a fallback + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageLoaderPNG.java b/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageLoaderPNG.java new file mode 100644 index 0000000..89d8998 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageLoaderPNG.java @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageLoaderPNG.java 1732019 2016-02-24 05:01:10Z gadams $ */ + +package org.apache.xmlgraphics.image.loader.impl; + +import java.awt.image.RenderedImage; +import java.io.IOException; +import java.util.Map; + +import javax.imageio.stream.ImageInputStream; +import javax.xml.transform.Source; + +import org.apache.xmlgraphics.image.codec.png.PNGDecodeParam; +import org.apache.xmlgraphics.image.codec.png.PNGImageDecoder; +import org.apache.xmlgraphics.image.codec.util.ImageInputStreamSeekableStreamAdapter; +import org.apache.xmlgraphics.image.codec.util.SeekableStream; +import org.apache.xmlgraphics.image.loader.Image; +import org.apache.xmlgraphics.image.loader.ImageException; +import org.apache.xmlgraphics.image.loader.ImageFlavor; +import org.apache.xmlgraphics.image.loader.ImageInfo; +import org.apache.xmlgraphics.image.loader.ImageSessionContext; +import org.apache.xmlgraphics.image.loader.util.ImageUtil; + +public class ImageLoaderPNG extends AbstractImageLoader { + + public ImageLoaderPNG() { + // + } + + /** {@inheritDoc} */ + public Image loadImage(ImageInfo info, Map hints, ImageSessionContext session) throws ImageException, + IOException { + + Source src = session.needSource(info.getOriginalURI()); + ImageInputStream imgStream = ImageUtil.needImageInputStream(src); + + SeekableStream seekStream = new ImageInputStreamSeekableStreamAdapter(imgStream); + + PNGImageDecoder decoder = new PNGImageDecoder(seekStream, new PNGDecodeParam()); + RenderedImage image = decoder.decodeAsRenderedImage(); + + // need transparency here? + return new ImageRendered(info, image, null); + } + + /** {@inheritDoc} */ + public ImageFlavor getTargetFlavor() { + return ImageFlavor.RENDERED_IMAGE; + } + + /** {@inheritDoc} */ + public int getUsagePenalty() { + // since this image loader does not provide any benefits over the default sun.imageio one we add + // some penalty to it so that it is not chosen by default; instead users need to give it a negative + // penalty in fop.xconf so that it is used; this image loader is mostly for testing purposes for now. + return 1000; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageLoaderRaw.java b/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageLoaderRaw.java new file mode 100644 index 0000000..7573267 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageLoaderRaw.java @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageLoaderRaw.java 1391005 2012-09-27 13:39:57Z mehdi $ */ + +package org.apache.xmlgraphics.image.loader.impl; + +import java.io.IOException; +import java.util.Map; + +import javax.xml.transform.Source; + +import org.apache.xmlgraphics.image.loader.Image; +import org.apache.xmlgraphics.image.loader.ImageException; +import org.apache.xmlgraphics.image.loader.ImageFlavor; +import org.apache.xmlgraphics.image.loader.ImageInfo; +import org.apache.xmlgraphics.image.loader.ImageSessionContext; +import org.apache.xmlgraphics.io.XmlSourceUtil; + +/** + * ImageLoader for formats consumed "raw" (undecoded). Provides a raw/undecoded stream. + */ +public class ImageLoaderRaw extends AbstractImageLoader { + + private String mime; + private ImageFlavor targetFlavor; + + /** + * Main constructor. + * @param targetFlavor the target flavor + */ + public ImageLoaderRaw(ImageFlavor targetFlavor) { + this.targetFlavor = targetFlavor; + this.mime = ImageLoaderFactoryRaw.getMimeForRawFlavor(targetFlavor); + } + + /** {@inheritDoc} */ + public ImageFlavor getTargetFlavor() { + return this.targetFlavor; + } + + /** {@inheritDoc} */ + public Image loadImage(ImageInfo info, Map hints, ImageSessionContext session) + throws ImageException, IOException { + if (!this.mime.equals(info.getMimeType())) { + throw new IllegalArgumentException( + "ImageInfo must be from a image with MIME type: " + this.mime); + } + Source src = session.needSource(info.getOriginalURI()); + ImageRawStream rawImage = new ImageRawStream(info, getTargetFlavor(), + XmlSourceUtil.needInputStream(src)); + return rawImage; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageLoaderRawCCITTFax.java b/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageLoaderRawCCITTFax.java new file mode 100644 index 0000000..238fcdc --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageLoaderRawCCITTFax.java @@ -0,0 +1,223 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageLoaderRawCCITTFax.java 1681108 2015-05-22 13:26:12Z ssteiner $ */ + +package org.apache.xmlgraphics.image.loader.impl; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; + +import javax.imageio.stream.ImageInputStream; +import javax.xml.transform.Source; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.apache.xmlgraphics.image.codec.tiff.TIFFDirectory; +import org.apache.xmlgraphics.image.codec.tiff.TIFFField; +import org.apache.xmlgraphics.image.codec.tiff.TIFFImage; +import org.apache.xmlgraphics.image.codec.tiff.TIFFImageDecoder; +import org.apache.xmlgraphics.image.codec.util.SeekableStream; +import org.apache.xmlgraphics.image.loader.Image; +import org.apache.xmlgraphics.image.loader.ImageException; +import org.apache.xmlgraphics.image.loader.ImageFlavor; +import org.apache.xmlgraphics.image.loader.ImageInfo; +import org.apache.xmlgraphics.image.loader.ImageSessionContext; +import org.apache.xmlgraphics.image.loader.util.ImageUtil; +import org.apache.xmlgraphics.image.loader.util.SeekableStreamAdapter; +import org.apache.xmlgraphics.io.XmlSourceUtil; +import org.apache.xmlgraphics.util.MimeConstants; +import org.apache.xmlgraphics.util.io.SubInputStream; + +/** + * ImageLoader for CCITT group 3 and 4 images consumed "raw" (undecoded). Provides a + * raw/undecoded stream. + */ +public class ImageLoaderRawCCITTFax extends AbstractImageLoader implements JPEGConstants { + + private static final int COMPRESSION_CCITT_1D = 2; + private static final int COMPRESSION_FAX_GROUP3 = 3; + private static final int COMPRESSION_FAX_GROUP4 = 4; + + /** logger */ + protected static final Log log = LogFactory.getLog(ImageLoaderRawCCITTFax.class); + + /** + * Main constructor. + */ + public ImageLoaderRawCCITTFax() { + } + + /** {@inheritDoc} */ + public ImageFlavor getTargetFlavor() { + return ImageFlavor.RAW_CCITTFAX; + } + + /** {@inheritDoc} */ + public Image loadImage(ImageInfo info, Map hints, ImageSessionContext session) + throws ImageException, IOException { + if (!MimeConstants.MIME_TIFF.equals(info.getMimeType())) { + throw new IllegalArgumentException("ImageInfo must be from a image with MIME type: " + + MimeConstants.MIME_TIFF); + } + int fillOrder = 1; + int compression = TIFFImage.COMP_NONE; + long stripOffset; + long stripLength; + TIFFDirectory dir; + + Source src = session.needSource(info.getOriginalURI()); + ImageInputStream in = ImageUtil.needImageInputStream(src); + in.mark(); + try { + SeekableStream seekable = new SeekableStreamAdapter(in); + dir = new TIFFDirectory(seekable, 0); + TIFFField fld; + + fld = dir.getField(TIFFImageDecoder.TIFF_COMPRESSION); + if (fld != null) { + compression = fld.getAsInt(0); + switch (compression) { + case COMPRESSION_CCITT_1D: + case COMPRESSION_FAX_GROUP4: + break; + case COMPRESSION_FAX_GROUP3: + //Note: the TIFFImage compression constants seem to be a bit misleading! + compression = TIFFImage.COMP_FAX_G3_1D; //1D is the default for Group3 + fld = dir.getField(TIFFImageDecoder.TIFF_T4_OPTIONS); + if (fld != null) { + long t4Options = fld.getAsLong(0); + if ((t4Options & 0x01) != 0) { + compression = TIFFImage.COMP_FAX_G3_2D; //"Abusing" for 2D signalling + } + } + break; + default: + log.debug("Unsupported compression " + compression); + throw new ImageException( + "ImageLoader doesn't support TIFF compression: " + compression); + } + } + //Read information used for raw embedding + fld = dir.getField(TIFFImageDecoder.TIFF_FILL_ORDER); + if (fld != null) { + fillOrder = fld.getAsInt(0); + } + + int stripCount; + fld = dir.getField(TIFFImageDecoder.TIFF_ROWS_PER_STRIP); + if (fld == null) { + stripCount = 1; + } else { + stripCount = (int)(info.getSize().getHeightPx() / fld.getAsLong(0)); + } + if (stripCount > 1) { + log.debug("More than one strip found in TIFF image."); + throw new ImageException( + "ImageLoader doesn't support multiple strips"); + } + stripOffset = dir.getField(TIFFImageDecoder.TIFF_STRIP_OFFSETS).getAsLong(0); + stripLength = dir.getField(TIFFImageDecoder.TIFF_STRIP_BYTE_COUNTS).getAsLong(0); + } finally { + in.reset(); + } + + in.seek(stripOffset); + InputStream subin = new SubInputStream(XmlSourceUtil.needInputStream(src), stripLength, true); + if (fillOrder == 2) { + //Decorate to flip bit order + subin = new FillOrderChangeInputStream(subin); + } + ImageRawCCITTFax rawImage = new ImageRawCCITTFax(info, subin, compression); + //Strip stream from source as we pass it on internally + XmlSourceUtil.removeStreams(src); + return rawImage; + } + + private static class FillOrderChangeInputStream extends FilterInputStream { + + protected FillOrderChangeInputStream(InputStream in) { + super(in); + } + + /** {@inheritDoc} */ + @Override + public int read(byte[] b, int off, int len) throws IOException { + int result = super.read(b, off, len); + if (result > 0) { + int endpos = off + result; + for (int i = off; i < endpos; i++) { + b[i] = FLIP_TABLE[b[i] & 0xff]; + } + } + return result; + } + + /** {@inheritDoc} */ + @Override + public int read() throws IOException { + int b = super.read(); + if (b < 0) { + return b; + } else { + return FLIP_TABLE[b] & 0xff; + } + } + + // Table to be used when fillOrder = 2, for flipping bytes. + // Copied from XML Graphics Commons' TIFFFaxDecoder class + private static final byte[] FLIP_TABLE = { + 0, -128, 64, -64, 32, -96, 96, -32, + 16, -112, 80, -48, 48, -80, 112, -16, + 8, -120, 72, -56, 40, -88, 104, -24, + 24, -104, 88, -40, 56, -72, 120, -8, + 4, -124, 68, -60, 36, -92, 100, -28, + 20, -108, 84, -44, 52, -76, 116, -12, + 12, -116, 76, -52, 44, -84, 108, -20, + 28, -100, 92, -36, 60, -68, 124, -4, + 2, -126, 66, -62, 34, -94, 98, -30, + 18, -110, 82, -46, 50, -78, 114, -14, + 10, -118, 74, -54, 42, -86, 106, -22, + 26, -102, 90, -38, 58, -70, 122, -6, + 6, -122, 70, -58, 38, -90, 102, -26, + 22, -106, 86, -42, 54, -74, 118, -10, + 14, -114, 78, -50, 46, -82, 110, -18, + 30, -98, 94, -34, 62, -66, 126, -2, + 1, -127, 65, -63, 33, -95, 97, -31, + 17, -111, 81, -47, 49, -79, 113, -15, + 9, -119, 73, -55, 41, -87, 105, -23, + 25, -103, 89, -39, 57, -71, 121, -7, + 5, -123, 69, -59, 37, -91, 101, -27, + 21, -107, 85, -43, 53, -75, 117, -11, + 13, -115, 77, -51, 45, -83, 109, -19, + 29, -99, 93, -35, 61, -67, 125, -3, + 3, -125, 67, -61, 35, -93, 99, -29, + 19, -109, 83, -45, 51, -77, 115, -13, + 11, -117, 75, -53, 43, -85, 107, -21, + 27, -101, 91, -37, 59, -69, 123, -5, + 7, -121, 71, -57, 39, -89, 103, -25, + 23, -105, 87, -41, 55, -73, 119, -9, + 15, -113, 79, -49, 47, -81, 111, -17, + 31, -97, 95, -33, 63, -65, 127, -1, + }; + // end + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageLoaderRawJPEG.java b/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageLoaderRawJPEG.java new file mode 100644 index 0000000..ce69b82 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageLoaderRawJPEG.java @@ -0,0 +1,263 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageLoaderRawJPEG.java 1681108 2015-05-22 13:26:12Z ssteiner $ */ + +package org.apache.xmlgraphics.image.loader.impl; + +import java.awt.color.ColorSpace; +import java.awt.color.ICC_Profile; +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.io.IOException; +import java.util.Map; + +import javax.imageio.stream.ImageInputStream; +import javax.xml.transform.Source; + +import org.apache.commons.io.output.ByteArrayOutputStream; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.apache.xmlgraphics.image.loader.Image; +import org.apache.xmlgraphics.image.loader.ImageException; +import org.apache.xmlgraphics.image.loader.ImageFlavor; +import org.apache.xmlgraphics.image.loader.ImageInfo; +import org.apache.xmlgraphics.image.loader.ImageSessionContext; +import org.apache.xmlgraphics.image.loader.util.ImageUtil; +import org.apache.xmlgraphics.io.XmlSourceUtil; +import org.apache.xmlgraphics.java2d.color.ColorSpaces; +import org.apache.xmlgraphics.java2d.color.profile.ColorProfileUtil; +import org.apache.xmlgraphics.util.MimeConstants; + +/** + * ImageLoader for JPEG images consumed "raw" (undecoded). Provides a + * raw/undecoded stream. + */ +public class ImageLoaderRawJPEG extends AbstractImageLoader implements JPEGConstants { + + /** logger */ + protected static final Log log = LogFactory.getLog(ImageLoaderRawJPEG.class); + + /** + * Main constructor. + */ + public ImageLoaderRawJPEG() { + } + + /** {@inheritDoc} */ + public ImageFlavor getTargetFlavor() { + return ImageFlavor.RAW_JPEG; + } + + /** {@inheritDoc} */ + public Image loadImage(ImageInfo info, Map hints, ImageSessionContext session) + throws ImageException, IOException { + if (!MimeConstants.MIME_JPEG.equals(info.getMimeType())) { + throw new IllegalArgumentException("ImageInfo must be from a image with MIME type: " + + MimeConstants.MIME_JPEG); + } + + ColorSpace colorSpace = null; + boolean appeFound = false; + int sofType = 0; + ByteArrayOutputStream iccStream = null; + + Source src = session.needSource(info.getOriginalURI()); + ImageInputStream in = ImageUtil.needImageInputStream(src); + JPEGFile jpeg = new JPEGFile(in); + in.mark(); + try { + outer: + while (true) { + int reclen; + int segID = jpeg.readMarkerSegment(); + if (log.isTraceEnabled()) { + log.trace("Seg Marker: " + Integer.toHexString(segID)); + } + switch (segID) { + case EOI: + log.trace("EOI found. Stopping."); + break outer; + case SOS: + log.trace("SOS found. Stopping early."); //TODO Not sure if this is safe + break outer; + case SOI: + case NULL: + break; + case SOF0: //baseline + case SOF1: //extended sequential DCT + case SOF2: //progressive (since PDF 1.3) + case SOFA: //progressive (since PDF 1.3) + sofType = segID; + if (log.isTraceEnabled()) { + log.trace("SOF: " + Integer.toHexString(sofType)); + } + in.mark(); + try { + reclen = jpeg.readSegmentLength(); + in.skipBytes(1); //data precision + in.skipBytes(2); //height + in.skipBytes(2); //width + int numComponents = in.readUnsignedByte(); + if (numComponents == 1) { + colorSpace = ColorSpace.getInstance( + ColorSpace.CS_GRAY); + } else if (numComponents == 3) { + colorSpace = ColorSpace.getInstance( + ColorSpace.CS_LINEAR_RGB); + } else if (numComponents == 4) { + colorSpace = ColorSpaces.getDeviceCMYKColorSpace(); + } else { + throw new ImageException("Unsupported ColorSpace for image " + + info + + ". The number of components supported are 1, 3 and 4."); + } + } finally { + in.reset(); + } + in.skipBytes(reclen); + break; + case APP2: //ICC (see ICC1V42.pdf) + in.mark(); + try { + reclen = jpeg.readSegmentLength(); + // Check for ICC profile + byte[] iccString = new byte[11]; + in.readFully(iccString); + in.skipBytes(1); //string terminator (null byte) + + if ("ICC_PROFILE".equals(new String(iccString, "US-ASCII"))) { + in.skipBytes(2); //chunk sequence number and total number of chunks + int payloadSize = reclen - 2 - 12 - 2; + if (ignoreColorProfile(hints)) { + log.debug("Ignoring ICC profile data in JPEG"); + in.skipBytes(payloadSize); + } else { + byte[] buf = new byte[payloadSize]; + in.readFully(buf); + if (iccStream == null) { + if (log.isDebugEnabled()) { + log.debug("JPEG has an ICC profile"); + DataInputStream din = new DataInputStream(new ByteArrayInputStream(buf)); + log.debug("Declared ICC profile size: " + din.readInt()); + } + //ICC profiles can be split into several chunks + //so collect in a byte array output stream + iccStream = new ByteArrayOutputStream(); + } + iccStream.write(buf); + } + } + } finally { + in.reset(); + } + in.skipBytes(reclen); + break; + case APPE: //Adobe-specific (see 5116.DCT_Filter.pdf) + in.mark(); + try { + reclen = jpeg.readSegmentLength(); + // Check for Adobe header + byte[] adobeHeader = new byte[5]; + in.readFully(adobeHeader); + + if ("Adobe".equals(new String(adobeHeader, "US-ASCII"))) { + // The reason for reading the APPE marker is that Adobe Photoshop + // generates CMYK JPEGs with inverted values. The correct thing + // to do would be to interpret the values in the marker, but for now + // only assume that if APPE marker is present and colorspace is CMYK, + // the image is inverted. + appeFound = true; + } + } finally { + in.reset(); + } + in.skipBytes(reclen); + break; + default: + jpeg.skipCurrentMarkerSegment(); + } + } + } finally { + in.reset(); + } + + ICC_Profile iccProfile = buildICCProfile(info, colorSpace, iccStream); + if (iccProfile == null && colorSpace == null) { + throw new ImageException("ColorSpace could not be identified for JPEG image " + info); + } + + boolean invertImage = false; + if (appeFound && colorSpace.getType() == ColorSpace.TYPE_CMYK) { + if (log.isDebugEnabled()) { + log.debug("JPEG has an Adobe APPE marker. Note: CMYK Image will be inverted. (" + + info.getOriginalURI() + ")"); + } + invertImage = true; + } + + ImageRawJPEG rawImage = new ImageRawJPEG(info, + XmlSourceUtil.needInputStream(src), + sofType, colorSpace, iccProfile, invertImage); + return rawImage; + } + + private ICC_Profile buildICCProfile(ImageInfo info, ColorSpace colorSpace, + ByteArrayOutputStream iccStream) throws IOException, ImageException { + if (iccStream != null && iccStream.size() > 0) { + if (log.isDebugEnabled()) { + log.debug("Effective ICC profile size: " + iccStream.size()); + } + final int alignment = 4; + int padding = (alignment - (iccStream.size() % alignment)) % alignment; + if (padding != 0) { + try { + iccStream.write(new byte[padding]); + } catch (IOException ioe) { + throw new IOException("Error while aligning ICC stream: " + ioe.getMessage()); + } + } + + ICC_Profile iccProfile = null; + try { + iccProfile = ColorProfileUtil.getICC_Profile(iccStream.toByteArray()); + if (log.isDebugEnabled()) { + log.debug("JPEG has an ICC profile: " + iccProfile.toString()); + } + } catch (IllegalArgumentException iae) { + log.warn("An ICC profile is present in the JPEG file but it is invalid (" + + iae.getMessage() + "). The color profile will be ignored. (" + + info.getOriginalURI() + ")"); + return null; + } + if (iccProfile.getNumComponents() != colorSpace.getNumComponents()) { + log.warn("The number of components of the ICC profile (" + + iccProfile.getNumComponents() + + ") doesn't match the image (" + + colorSpace.getNumComponents() + + "). Ignoring the ICC color profile."); + return null; + } else { + return iccProfile; + } + } else { + return null; //no ICC profile available + } + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageLoaderRawPNG.java b/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageLoaderRawPNG.java new file mode 100644 index 0000000..87a8ada --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageLoaderRawPNG.java @@ -0,0 +1,86 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageLoaderRawPNG.java 1843559 2018-10-11 14:59:17Z ssteiner $ */ + +// Original author: Matthias Reichenbacher + +package org.apache.xmlgraphics.image.loader.impl; + +import java.io.IOException; +import java.util.Map; + +import javax.imageio.stream.ImageInputStream; +import javax.xml.transform.Source; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.apache.xmlgraphics.image.codec.util.ImageInputStreamSeekableStreamAdapter; +import org.apache.xmlgraphics.image.codec.util.SeekableStream; +import org.apache.xmlgraphics.image.loader.Image; +import org.apache.xmlgraphics.image.loader.ImageException; +import org.apache.xmlgraphics.image.loader.ImageFlavor; +import org.apache.xmlgraphics.image.loader.ImageInfo; +import org.apache.xmlgraphics.image.loader.ImageSessionContext; +import org.apache.xmlgraphics.image.loader.util.ImageUtil; +import org.apache.xmlgraphics.io.XmlSourceUtil; +import org.apache.xmlgraphics.util.MimeConstants; + +public class ImageLoaderRawPNG extends AbstractImageLoader { + + /** logger */ + protected static final Log log = LogFactory.getLog(ImageLoaderRawPNG.class); + + /** + * Main constructor. + */ + public ImageLoaderRawPNG() { + } + + /** {@inheritDoc} */ + public ImageFlavor getTargetFlavor() { + return ImageFlavor.RAW_PNG; + } + + /** {@inheritDoc} */ + public Image loadImage(ImageInfo info, Map hints, ImageSessionContext session) throws ImageException, + IOException { + if (!MimeConstants.MIME_PNG.equals(info.getMimeType())) { + throw new IllegalArgumentException("ImageInfo must be from a image with MIME type: " + + MimeConstants.MIME_PNG); + } + + Source src = session.needSource(info.getOriginalURI()); + ImageInputStream in = ImageUtil.needImageInputStream(src); + // Remove streams as we do things with them at some later time. + XmlSourceUtil.removeStreams(src); + SeekableStream seekStream = new ImageInputStreamSeekableStreamAdapter(in); + PNGFile im = new PNGFile(seekStream, info.getOriginalURI()); + ImageRawPNG irpng = im.getImageRawPNG(info); + return irpng; + } + + /** {@inheritDoc} */ + public int getUsagePenalty() { + // since this image loader does not handle all kinds of PNG images then we add some penalty to it + // so that it is not chosen by default; instead, users need to give it a negative penalty in + // fop.xconf so that it is used + return 1000; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageRawCCITTFax.java b/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageRawCCITTFax.java new file mode 100644 index 0000000..4321fa6 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageRawCCITTFax.java @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageRawCCITTFax.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.image.loader.impl; + +import java.awt.color.ColorSpace; + +import org.apache.xmlgraphics.image.loader.ImageFlavor; +import org.apache.xmlgraphics.image.loader.ImageInfo; + +/** + * This class is an implementation of the Image interface exposing a 1-bit bitmap image stream + * that can be decoded by the PostScript or PDF CCITTFaxDecode filter. + */ +public class ImageRawCCITTFax extends ImageRawStream { + + private int compression; + + /** + * Main constructor. + * @param info the image info object + * @param in the ImageInputStream with the raw content + * @param compression the integer value of the TIFF compression scheme + */ + public ImageRawCCITTFax(ImageInfo info, java.io.InputStream in, int compression) { + super(info, ImageFlavor.RAW_CCITTFAX, in); + this.compression = compression; + } + + /** + * Returns the image's color space + * @return the color space + */ + public ColorSpace getColorSpace() { + return ColorSpace.getInstance(ColorSpace.CS_GRAY); + } + + /** + * Returns the TIFF compression scheme. + * @return the TIFF compression scheme + */ + public int getCompression() { + return this.compression; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageRawEPS.java b/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageRawEPS.java new file mode 100644 index 0000000..a6f35bb --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageRawEPS.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageRawEPS.java 722804 2008-12-03 08:06:44Z jeremias $ */ + +package org.apache.xmlgraphics.image.loader.impl; + +import java.awt.geom.Rectangle2D; +import java.io.InputStream; + +import org.apache.xmlgraphics.image.loader.ImageFlavor; +import org.apache.xmlgraphics.image.loader.ImageInfo; + +/** + * This class is an implementation of the Image interface exposing EPS file. It provides an + * InputStream to access the EPS content and the decoded high-res bounding box. + */ +public class ImageRawEPS extends ImageRawStream { + + /** + * Main constructor. + * @param info the image info object + * @param streamFactory the InputStreamFactory that is used to create InputStream instances + */ + public ImageRawEPS(ImageInfo info, InputStreamFactory streamFactory) { + super(info, ImageFlavor.RAW_EPS, streamFactory); + } + + /** + * Main constructor. + * @param info the image info object + * @param in the InputStream with the raw content + */ + public ImageRawEPS(ImageInfo info, InputStream in) { + super(info, ImageFlavor.RAW_EPS, in); + } + + /** + * Returns the bounding box of the EPS image. + * @return the bounding box + */ + public Rectangle2D getBoundingBox() { + Rectangle2D bbox = (Rectangle2D)getInfo().getCustomObjects().get( + PreloaderEPS.EPS_BOUNDING_BOX); + return bbox; + } +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageRawJPEG.java b/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageRawJPEG.java new file mode 100644 index 0000000..368844b --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageRawJPEG.java @@ -0,0 +1,89 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageRawJPEG.java 1610846 2014-07-15 20:44:18Z vhennebert $ */ + +package org.apache.xmlgraphics.image.loader.impl; + +import java.awt.color.ColorSpace; +import java.awt.color.ICC_Profile; + +import org.apache.xmlgraphics.image.loader.ImageFlavor; +import org.apache.xmlgraphics.image.loader.ImageInfo; + +/** + * This class is an implementation of the Image interface exposing a JPEG file. It provides an + * InputStream to access the JPEG content and some additional information on the image. + */ +public class ImageRawJPEG extends ImageRawStream { + + private int sofType; + private ColorSpace colorSpace; + private ICC_Profile iccProfile; + private boolean invertImage; + + /** + * Main constructor. + * @param info the image info object + * @param in the ImageInputStream with the raw content + * @param sofType the SOFn identifier + * @param colorSpace the color space + * @param iccProfile an ICC color profile or null if no profile is associated + * @param invertImage true if the image should be inverted when painting it + */ + public ImageRawJPEG(ImageInfo info, java.io.InputStream in, + int sofType, ColorSpace colorSpace, ICC_Profile iccProfile, boolean invertImage) { + super(info, ImageFlavor.RAW_JPEG, in); + this.sofType = sofType; + this.colorSpace = colorSpace; + this.iccProfile = iccProfile; + this.invertImage = invertImage; + } + + /** + * Returns the SOFn identifier of the image which describes the coding format of the image. + * @return the SOFn identifier + */ + public int getSOFType() { + return this.sofType; + } + + /** + * Returns the ICC color profile if one is associated with the JPEG image. + * @return the ICC color profile or null if there's no profile + */ + public ICC_Profile getICCProfile() { + return this.iccProfile; + } + + /** + * Indicates whether the image should be inverted when interpreting it. + * @return true if the image is to be inverted + */ + public boolean isInverted() { + return this.invertImage; + } + + /** + * Returns the image's color space + * @return the color space + */ + public ColorSpace getColorSpace() { + return this.colorSpace; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageRawPNG.java b/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageRawPNG.java new file mode 100644 index 0000000..517699a --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageRawPNG.java @@ -0,0 +1,162 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageRawPNG.java 1732019 2016-02-24 05:01:10Z gadams $ */ + +// Original author: Matthias Reichenbacher + +package org.apache.xmlgraphics.image.loader.impl; + +import java.awt.Color; +import java.awt.color.ColorSpace; +import java.awt.color.ICC_Profile; +import java.awt.image.ColorModel; +import java.io.InputStream; + +import org.apache.xmlgraphics.image.loader.ImageFlavor; +import org.apache.xmlgraphics.image.loader.ImageInfo; + +public class ImageRawPNG extends ImageRawStream { + + private ColorModel cm; + private ICC_Profile iccProfile; + private int bitDepth; + private boolean isTransparent; + private int grayTransparentAlpha; + private int redTransparentAlpha; + private int greenTransparentAlpha; + private int blueTransparentAlpha; + private int renderingIntent = -1; + + /** + * Main constructor. + * @param info the image info object + * @param in the ImageInputStream with the raw content + * @param colorModel the color model + * @param bitDepth the bit depth + * @param iccProfile an ICC color profile or null if no profile is associated + */ + public ImageRawPNG(ImageInfo info, InputStream in, ColorModel colorModel, int bitDepth, ICC_Profile iccProfile) { + super(info, ImageFlavor.RAW_PNG, in); + this.iccProfile = iccProfile; + this.cm = colorModel; + this.bitDepth = bitDepth; + } + + /** + * The bit depth of each color channel. + * @return the bit depth of one channel (same for all) + */ + public int getBitDepth() { + return bitDepth; + } + + /** + * Returns the ICC color profile if one is associated with the PNG image. + * @return the ICC color profile or null if there's no profile + */ + public ICC_Profile getICCProfile() { + return this.iccProfile; + } + + /** + * Returns the image's color model. + * @return the color model + */ + public ColorModel getColorModel() { + return this.cm; + } + + /** + * Returns the image's color space. + * @return the color space + */ + public ColorSpace getColorSpace() { + return this.cm.getColorSpace(); + } + + /** + * Sets the gray transparent pixel value. + * @param gray the transparent pixel gray value (0...255) + */ + protected void setGrayTransparentAlpha(int gray) { + this.isTransparent = true; + this.grayTransparentAlpha = gray; + } + + /** + * Sets the RGB transparent pixel values. + * @param red the transparent pixel red value (0...255) + * @param green the transparent pixel green value (0...255) + * @param blue the transparent pixel blue value (0...255) + */ + protected void setRGBTransparentAlpha(int red, int green, int blue) { + this.isTransparent = true; + this.redTransparentAlpha = red; + this.greenTransparentAlpha = green; + this.blueTransparentAlpha = blue; + } + + /** + * Used to flag image as transparent when the image is of pallete type. + */ + protected void setTransparent() { + this.isTransparent = true; + } + + /** + * Whether the image is transparent (meaning there is a transparent pixel) + * @return true if transparent pixel exists + */ + public boolean isTransparent() { + return this.isTransparent; + } + + /** + * The color of the transparent pixel. + * @return the color of the transparent pixel. + */ + public Color getTransparentColor() { + Color color = null; + if (!this.isTransparent) { + return color; + } + if (cm.getNumColorComponents() == 3) { + color = new Color(this.redTransparentAlpha, this.greenTransparentAlpha, this.blueTransparentAlpha); + } else { + color = new Color(this.grayTransparentAlpha, 0, 0); + } + return color; + } + + /** + * Used to set the rendering intent when the color space is sRGB. + * @param intent the rendering intent of the sRGB color space + */ + public void setRenderingIntent(int intent) { + renderingIntent = intent; + } + + /** + * Returns the rendering intent of the sRGB color space. + * @return the rendering intent + */ + public int getRenderingIntent() { + return this.renderingIntent; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageRawStream.java b/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageRawStream.java new file mode 100644 index 0000000..be1e3bf --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageRawStream.java @@ -0,0 +1,216 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageRawStream.java 1681137 2015-05-22 14:54:05Z ssteiner $ */ + +package org.apache.xmlgraphics.image.loader.impl; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.apache.commons.io.IOUtils; + +import org.apache.xmlgraphics.image.loader.ImageFlavor; +import org.apache.xmlgraphics.image.loader.ImageInfo; +import org.apache.xmlgraphics.image.loader.MimeEnabledImageFlavor; + +/** + * This class is an implementation of the Image interface exposing an InputStream for loading the + * raw/undecoded image. + */ +public class ImageRawStream extends AbstractImage { + + private ImageFlavor flavor; + private InputStreamFactory streamFactory; + + /** + * Main constructor. + * @param info the image info object + * @param flavor the image flavor for the raw image + * @param streamFactory the InputStreamFactory that is used to create InputStream instances + */ + public ImageRawStream(ImageInfo info, ImageFlavor flavor, InputStreamFactory streamFactory) { + super(info); + this.flavor = flavor; + setInputStreamFactory(streamFactory); + } + + /** + * Constructor for a simple InputStream as parameter. + * @param info the image info object + * @param flavor the image flavor for the raw image + * @param in the InputStream with the raw content + */ + public ImageRawStream(ImageInfo info, ImageFlavor flavor, InputStream in) { + this(info, flavor, new SingleStreamFactory(in)); + } + + /** {@inheritDoc} */ + public ImageFlavor getFlavor() { + return this.flavor; + } + + /** + * Returns the MIME type of the stream data. + * @return the MIME type + */ + public String getMimeType() { + if (getFlavor() instanceof MimeEnabledImageFlavor) { + return getFlavor().getMimeType(); + } else { + //Undetermined + return "application/octet-stream"; + } + } + + /** {@inheritDoc} */ + public boolean isCacheable() { + return !this.streamFactory.isUsedOnceOnly(); + } + + /** + * Sets the InputStreamFactory to be used by this image. This method allows to replace the + * original factory. + * @param factory the new InputStreamFactory + */ + public void setInputStreamFactory(InputStreamFactory factory) { + if (this.streamFactory != null) { + this.streamFactory.close(); + } + this.streamFactory = factory; + } + + /** + * Returns a new InputStream to access the raw image. + * @return the InputStream + */ + public InputStream createInputStream() { + return this.streamFactory.createInputStream(); + } + + /** + * Writes the content of the image to an OutputStream. The OutputStream in NOT closed at the + * end. + * @param out the OutputStream + * @throws IOException if an I/O error occurs + */ + public void writeTo(OutputStream out) throws IOException { + InputStream in = createInputStream(); + try { + IOUtils.copy(in, out); + } finally { + IOUtils.closeQuietly(in); + } + } + + /** + * Represents a factory for InputStream objects. Make sure the class is thread-safe! + */ + public interface InputStreamFactory { + + /** + * Indicates whether this factory is only usable once or many times. + * @return true if the factory can only be used once + */ + boolean isUsedOnceOnly(); + + /** + * Creates and returns a new InputStream. + * @return the new InputStream + */ + InputStream createInputStream(); + + /** + * Closes the factory and releases any resources held open during the lifetime of this + * object. + */ + void close(); + + } + + /** + * InputStream factory that can return a pre-constructed InputStream exactly once. + */ + private static class SingleStreamFactory implements InputStreamFactory { + + private InputStream in; + + public SingleStreamFactory(InputStream in) { + this.in = in; + } + + public synchronized InputStream createInputStream() { + if (this.in != null) { + InputStream tempin = this.in; + this.in = null; //Don't close, just remove the reference + return tempin; + } else { + throw new IllegalStateException("Can only create an InputStream once!"); + } + } + + public synchronized void close() { + IOUtils.closeQuietly(this.in); + this.in = null; + } + + public boolean isUsedOnceOnly() { + return true; + } + + /** {@inheritDoc} */ + protected void finalize() { + close(); + } + + } + + /** + * InputStream factory that wraps a byte array. + */ + public static class ByteArrayStreamFactory implements InputStreamFactory { + + private byte[] data; + + /** + * Main constructor. + * @param data the byte array + */ + public ByteArrayStreamFactory(byte[] data) { + this.data = data; + } + + /** {@inheritDoc} */ + public InputStream createInputStream() { + return new ByteArrayInputStream(data); + } + + /** {@inheritDoc} */ + public void close() { + //nop + } + + /** {@inheritDoc} */ + public boolean isUsedOnceOnly() { + return false; + } + + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageRendered.java b/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageRendered.java new file mode 100644 index 0000000..3aa4c5c --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageRendered.java @@ -0,0 +1,96 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageRendered.java 995366 2010-09-09 10:02:17Z jeremias $ */ + +package org.apache.xmlgraphics.image.loader.impl; + +import java.awt.Color; +import java.awt.color.ColorSpace; +import java.awt.color.ICC_ColorSpace; +import java.awt.color.ICC_Profile; +import java.awt.image.RenderedImage; + +import org.apache.xmlgraphics.image.loader.ImageFlavor; +import org.apache.xmlgraphics.image.loader.ImageInfo; + +/** + * This class is an implementation of the Image interface exposing a RenderedImage. + */ +public class ImageRendered extends AbstractImage { + + private final RenderedImage red; + private final Color transparentColor; + private final ColorSpace colorSpace; + private final ICC_Profile iccProfile; + + /** + * Main constructor. + * @param info the image info object + * @param red the RenderedImage instance + * @param transparentColor the transparent color or null + */ + public ImageRendered(ImageInfo info, RenderedImage red, Color transparentColor) { + super(info); + this.red = red; + this.transparentColor = transparentColor; + this.colorSpace = red.getColorModel().getColorSpace(); + if (this.colorSpace instanceof ICC_ColorSpace) { + ICC_ColorSpace icccs = (ICC_ColorSpace) this.colorSpace; + this.iccProfile = icccs.getProfile(); + } else { + this.iccProfile = null; + } + } + + /** {@inheritDoc} */ + public ImageFlavor getFlavor() { + return ImageFlavor.RENDERED_IMAGE; + } + + /** {@inheritDoc} */ + public boolean isCacheable() { + return true; + } + + /** + * Returns the contained RenderedImage instance. + * @return the RenderedImage instance + */ + public RenderedImage getRenderedImage() { + return this.red; + } + + /** {@inheritDoc} */ + public ColorSpace getColorSpace() { + return this.colorSpace; + } + + /** {@inheritDoc} */ + public ICC_Profile getICCProfile() { + return this.iccProfile; + } + + /** + * Returns the transparent color if available. + * @return the transparent color or null + */ + public Color getTransparentColor() { + return this.transparentColor; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageXMLDOM.java b/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageXMLDOM.java new file mode 100644 index 0000000..4d2a1d4 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/impl/ImageXMLDOM.java @@ -0,0 +1,89 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageXMLDOM.java 682720 2008-08-05 14:22:29Z jeremias $ */ + +package org.apache.xmlgraphics.image.loader.impl; + +import org.w3c.dom.Document; + +import org.apache.xmlgraphics.image.loader.ImageFlavor; +import org.apache.xmlgraphics.image.loader.ImageInfo; +import org.apache.xmlgraphics.image.loader.XMLNamespaceEnabledImageFlavor; + +/** + * This class is an implementation of the Image interface exposing an XML DOM (W3C). + */ +public class ImageXMLDOM extends AbstractImage { + + private ImageFlavor flavor; + private Document doc; + private String rootNamespace; + + /** + * Main constructor. + * @param info the image info object + * @param doc the W3C DOM document + * @param rootNamespace the root XML namespace of the XML document in the DOM + */ + public ImageXMLDOM(ImageInfo info, Document doc, String rootNamespace) { + super(info); + this.doc = doc; + this.rootNamespace = rootNamespace; + this.flavor = new XMLNamespaceEnabledImageFlavor(ImageFlavor.XML_DOM, rootNamespace); + } + + /** + * Main constructor. + * @param info the image info object + * @param doc the W3C DOM document + * @param flavor the image flavor + */ + public ImageXMLDOM(ImageInfo info, Document doc, XMLNamespaceEnabledImageFlavor flavor) { + super(info); + this.doc = doc; + this.rootNamespace = flavor.getNamespace(); + this.flavor = flavor; + } + + /** {@inheritDoc} */ + public ImageFlavor getFlavor() { + return this.flavor; + } + + /** {@inheritDoc} */ + public boolean isCacheable() { + return true; + } + + /** + * Returns the contained W3C DOM document. + * @return the DOM + */ + public Document getDocument() { + return this.doc; + } + + /** + * Returns the root XML namespace of the XML document. + * @return the root namespace + */ + public String getRootNamespace() { + return this.rootNamespace; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/impl/JPEGConstants.java b/src/main/java/org/apache/xmlgraphics/image/loader/impl/JPEGConstants.java new file mode 100644 index 0000000..34f5f70 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/impl/JPEGConstants.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: JPEGConstants.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.image.loader.impl; + +/** + * Constants for JPEG images + */ +public interface JPEGConstants { + + /* + * Only SOFn and APPn markers are defined as SOFn is needed for the height + * and width search. APPn is also defined because if the JPEG contains + * thumbnails the dimensions of the thumbnail would also be after the SOFn + * marker enclosed inside the APPn marker. And we don't want to confuse + * those dimensions with the image dimensions. + */ + /** Beginning of a Marker */ + int MARK = 0xff; + /** Special case for 0xff00 */ + int NULL = 0x00; + /** Baseline DCT */ + int SOF0 = 0xc0; + /** Extended Sequential DCT */ + int SOF1 = 0xc1; + /** Progressive DCT only PDF 1.3 */ + int SOF2 = 0xc2; + /** Progressive DCT only PDF 1.3 */ + int SOFA = 0xca; + + /** Application marker, JFIF, JFXX, CIFF */ + int APP0 = 0xe0; + /** Application marker, EXIF, XMP */ + int APP1 = 0xe1; + /** Application marker, ICC, FlashPix */ + int APP2 = 0xe2; + /** Application marker APPD/APP13, Photoshop, Adobe_CM */ + int APPD = 0xed; + /** Application marker APPE/APP14, Adobe */ + int APPE = 0xee; + /** Application marker APPF/APP15, GraphicConverter */ + int APPF = 0xef; + + /** Start of Scan */ + int SOS = 0xda; + /** start of Image */ + int SOI = 0xd8; + /** end of Image */ + int EOI = 0xd9; + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/impl/JPEGFile.java b/src/main/java/org/apache/xmlgraphics/image/loader/impl/JPEGFile.java new file mode 100644 index 0000000..938dbb8 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/impl/JPEGFile.java @@ -0,0 +1,106 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: JPEGFile.java 1681108 2015-05-22 13:26:12Z ssteiner $ */ + +package org.apache.xmlgraphics.image.loader.impl; + +import java.io.DataInput; +import java.io.IOException; +import java.io.InputStream; + +import javax.imageio.stream.ImageInputStream; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Provides methods useful for processing JPEG files. + */ +public class JPEGFile implements JPEGConstants { + + /** logger */ + protected static final Log log = LogFactory.getLog(JPEGFile.class); + + private DataInput in; + + /** + * Constructor for ImageInputStreams. + * @param in the input stream to read the image from + */ + public JPEGFile(ImageInputStream in) { + this.in = in; + } + + /** + * Constructor for InputStreams. + * @param in the input stream to read the image from + */ + public JPEGFile(InputStream in) { + this.in = new java.io.DataInputStream(in); + } + + /** + * Returns the {@link DataInput} instance this object operates on. + * @return the data input instance + */ + public DataInput getDataInput() { + return this.in; + } + + /** + * Reads the next marker segment identifier and returns it. + * @return the marker segment identifier + * @throws IOException if an I/O error occurs while reading from the image file + */ + public int readMarkerSegment() throws IOException { + int marker; + do { + marker = in.readByte() & 0xFF; + //Skip any non-0xFF bytes (useful for JPEG files with bad record lengths) + } while (marker != MARK); + + int segID; + do { + segID = in.readByte() & 0xFF; + //Skip any pad bytes (0xFF) which are legal here. + } while (segID == 0xFF); + return segID; + } + + /** + * Reads the segment length of the current marker segment and returns it. + * The method assumes the file cursor is right after the segment header. + * @return the segment length + * @throws IOException if an I/O error occurs while reading from the image file + */ + public int readSegmentLength() throws IOException { + int reclen = in.readUnsignedShort(); + return reclen; + } + + /** + * Skips the current marker segment. + * The method assumes the file cursor is right after the segment header. + * @throws IOException if an I/O error occurs while reading from the image file + */ + public void skipCurrentMarkerSegment() throws IOException { + int reclen = readSegmentLength(); + in.skipBytes(reclen - 2); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/impl/PNGConstants.java b/src/main/java/org/apache/xmlgraphics/image/loader/impl/PNGConstants.java new file mode 100644 index 0000000..2207549 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/impl/PNGConstants.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PNGConstants.java 1732019 2016-02-24 05:01:10Z gadams $ */ + +package org.apache.xmlgraphics.image.loader.impl; + +public interface PNGConstants { + + /* + * First 8 bytes of any PNG file. + */ + long PNG_SIGNATURE = 0x89504e470d0a1a0aL; + + /* + * Color types. + */ + int PNG_COLOR_GRAY = 0; + int PNG_COLOR_RGB = 2; + int PNG_COLOR_PALETTE = 3; + int PNG_COLOR_GRAY_ALPHA = 4; + int PNG_COLOR_RGB_ALPHA = 6; + + /* + * Filter types. + */ + int PNG_FILTER_NONE = 0; + int PNG_FILTER_SUB = 1; + int PNG_FILTER_UP = 2; + int PNG_FILTER_AVERAGE = 3; + int PNG_FILTER_PAETH = 4; + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/impl/PNGFile.java b/src/main/java/org/apache/xmlgraphics/image/loader/impl/PNGFile.java new file mode 100644 index 0000000..c568630 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/impl/PNGFile.java @@ -0,0 +1,280 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PNGFile.java 1843559 2018-10-11 14:59:17Z ssteiner $ */ + +package org.apache.xmlgraphics.image.loader.impl; + +import java.awt.color.ColorSpace; +import java.awt.color.ICC_ColorSpace; +import java.awt.color.ICC_Profile; +import java.awt.image.ColorModel; +import java.awt.image.ComponentColorModel; +import java.awt.image.DataBuffer; +import java.awt.image.IndexColorModel; +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.SequenceInputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; + +import org.apache.xmlgraphics.image.codec.png.PNGChunk; +import org.apache.xmlgraphics.image.codec.util.PropertyUtil; +import org.apache.xmlgraphics.image.loader.ImageException; +import org.apache.xmlgraphics.image.loader.ImageInfo; + +// CSOFF: MethodName + +/** + * Provides methods useful for processing PNG files. + */ +class PNGFile implements PNGConstants { + + private ColorModel colorModel; + private ICC_Profile iccProfile; + private int sRGBRenderingIntent = -1; + private int bitDepth; + private int colorType; + private boolean isTransparent; + private int grayTransparentAlpha; + private int redTransparentAlpha; + private int greenTransparentAlpha; + private int blueTransparentAlpha; + private List streamVec = new ArrayList(); + private int paletteEntries; + private byte[] redPalette; + private byte[] greenPalette; + private byte[] bluePalette; + private byte[] alphaPalette; + private boolean hasPalette; + private boolean hasAlphaPalette; + + public PNGFile(InputStream stream, String uri) throws IOException, ImageException { + if (!stream.markSupported()) { + stream = new BufferedInputStream(stream); + } + DataInputStream distream = new DataInputStream(stream); + long magic = distream.readLong(); + if (magic != PNG_SIGNATURE) { + String msg = PropertyUtil.getString("PNGImageDecoder0"); + throw new ImageException(msg); + } + // only some chunks are worth parsing in the current implementation + do { + try { + PNGChunk chunk; + String chunkType = PNGChunk.getChunkType(distream); + if (chunkType.equals(PNGChunk.ChunkType.IHDR.name())) { + chunk = PNGChunk.readChunk(distream); + parse_IHDR_chunk(chunk); + } else if (chunkType.equals(PNGChunk.ChunkType.PLTE.name())) { + chunk = PNGChunk.readChunk(distream); + parse_PLTE_chunk(chunk); + } else if (chunkType.equals(PNGChunk.ChunkType.IDAT.name())) { + chunk = PNGChunk.readChunk(distream); + streamVec.add(new ByteArrayInputStream(chunk.getData())); + } else if (chunkType.equals(PNGChunk.ChunkType.IEND.name())) { + // chunk = PNGChunk.readChunk(distream); + PNGChunk.skipChunk(distream); + break; // fall through to the bottom + } else if (chunkType.equals(PNGChunk.ChunkType.tRNS.name())) { + chunk = PNGChunk.readChunk(distream); + parse_tRNS_chunk(chunk); + } else if (chunkType.equals(PNGChunk.ChunkType.iCCP.name())) { + chunk = PNGChunk.readChunk(distream); + parse_iCCP_chunk(chunk); + } else if (chunkType.equals(PNGChunk.ChunkType.sRGB.name())) { + chunk = PNGChunk.readChunk(distream); + parse_sRGB_chunk(chunk); + } else { + if (Character.isUpperCase(chunkType.charAt(0))) { + throw new ImageException("PNG unknown critical chunk: " + chunkType); + } + PNGChunk.skipChunk(distream); + } + } catch (Exception e) { + String msg = PropertyUtil.getString("PNGImageDecoder2"); + throw new RuntimeException(msg + " " + uri, e); + } + } while (true); + } + + public ImageRawPNG getImageRawPNG(ImageInfo info) throws ImageException { + InputStream seqStream = new SequenceInputStream(Collections.enumeration(streamVec)); + ColorSpace rgbCS = null; + switch (colorType) { + case PNG_COLOR_GRAY: + if (hasPalette) { + throw new ImageException("Corrupt PNG: color palette is not allowed!"); + } + colorModel = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_GRAY), false, false, + ColorModel.OPAQUE, DataBuffer.TYPE_BYTE); + break; + case PNG_COLOR_RGB: + if (iccProfile != null) { + rgbCS = new ICC_ColorSpace(iccProfile); + } else if (sRGBRenderingIntent != -1) { + rgbCS = ColorSpace.getInstance(ColorSpace.CS_sRGB); + } else { + rgbCS = ColorSpace.getInstance(ColorSpace.CS_LINEAR_RGB); + } + colorModel = new ComponentColorModel(rgbCS, false, false, ColorModel.OPAQUE, DataBuffer.TYPE_BYTE); + break; + case PNG_COLOR_PALETTE: + if (hasAlphaPalette) { + colorModel = new IndexColorModel(bitDepth, paletteEntries, redPalette, greenPalette, + bluePalette, alphaPalette); + } else { + colorModel = new IndexColorModel(bitDepth, paletteEntries, redPalette, greenPalette, + bluePalette); + } + break; + case PNG_COLOR_GRAY_ALPHA: + if (hasPalette) { + throw new ImageException("Corrupt PNG: color palette is not allowed!"); + } + colorModel = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_GRAY), true, false, + ColorModel.TRANSLUCENT, DataBuffer.TYPE_BYTE); + break; + case PNG_COLOR_RGB_ALPHA: + if (iccProfile != null) { + rgbCS = new ICC_ColorSpace(iccProfile); + } else if (sRGBRenderingIntent != -1) { + rgbCS = ColorSpace.getInstance(ColorSpace.CS_sRGB); + } else { + rgbCS = ColorSpace.getInstance(ColorSpace.CS_LINEAR_RGB); + } + colorModel = new ComponentColorModel(rgbCS, true, false, ColorModel.TRANSLUCENT, + DataBuffer.TYPE_BYTE); + break; + default: + throw new ImageException("Unsupported color type: " + colorType); + } + // the iccProfile is still null for now + ImageRawPNG rawImage = new ImageRawPNG(info, seqStream, colorModel, bitDepth, iccProfile); + if (isTransparent) { + if (colorType == PNG_COLOR_GRAY) { + rawImage.setGrayTransparentAlpha(grayTransparentAlpha); + } else if (colorType == PNG_COLOR_RGB) { + rawImage.setRGBTransparentAlpha(redTransparentAlpha, greenTransparentAlpha, + blueTransparentAlpha); + } else if (colorType == PNG_COLOR_PALETTE) { + rawImage.setTransparent(); + } else { + // + } + } + if (sRGBRenderingIntent != -1) { + rawImage.setRenderingIntent(sRGBRenderingIntent); + } + return rawImage; + } + + private void parse_IHDR_chunk(PNGChunk chunk) { + bitDepth = chunk.getInt1(8); + colorType = chunk.getInt1(9); + int compressionMethod = chunk.getInt1(10); + if (compressionMethod != 0) { + throw new RuntimeException("Unsupported PNG compression method: " + compressionMethod); + } + int filterMethod = chunk.getInt1(11); + if (filterMethod != 0) { + throw new RuntimeException("Unsupported PNG filter method: " + filterMethod); + } + int interlaceMethod = chunk.getInt1(12); + if (interlaceMethod != 0) { + // this is a limitation of the current implementation + throw new RuntimeException("Unsupported PNG interlace method: " + interlaceMethod); + } + } + + private void parse_PLTE_chunk(PNGChunk chunk) { + paletteEntries = chunk.getLength() / 3; + redPalette = new byte[paletteEntries]; + greenPalette = new byte[paletteEntries]; + bluePalette = new byte[paletteEntries]; + hasPalette = true; + + int pltIndex = 0; + for (int i = 0; i < paletteEntries; i++) { + redPalette[i] = chunk.getByte(pltIndex++); + greenPalette[i] = chunk.getByte(pltIndex++); + bluePalette[i] = chunk.getByte(pltIndex++); + } + } + + private void parse_tRNS_chunk(PNGChunk chunk) { + if (colorType == PNG_COLOR_PALETTE) { + int entries = chunk.getLength(); + if (entries > paletteEntries) { + // Error -- mustn't have more alpha than RGB palette entries + String msg = PropertyUtil.getString("PNGImageDecoder14"); + throw new RuntimeException(msg); + } + // Load beginning of palette from the chunk + alphaPalette = new byte[paletteEntries]; + for (int i = 0; i < entries; i++) { + alphaPalette[i] = chunk.getByte(i); + } + // Fill rest of palette with 255 + for (int i = entries; i < paletteEntries; i++) { + alphaPalette[i] = (byte) 255; + } + hasAlphaPalette = true; + } else if (colorType == PNG_COLOR_GRAY) { + grayTransparentAlpha = chunk.getInt2(0); + } else if (colorType == PNG_COLOR_RGB) { + redTransparentAlpha = chunk.getInt2(0); + greenTransparentAlpha = chunk.getInt2(2); + blueTransparentAlpha = chunk.getInt2(4); + } else if (colorType == PNG_COLOR_GRAY_ALPHA || colorType == PNG_COLOR_RGB_ALPHA) { + // Error -- GA or RGBA image can't have a tRNS chunk. + String msg = PropertyUtil.getString("PNGImageDecoder15"); + throw new RuntimeException(msg); + } + isTransparent = true; + } + + private void parse_iCCP_chunk(PNGChunk chunk) { + int length = chunk.getLength(); + int textIndex = 0; + while (chunk.getByte(textIndex++) != 0) { + //NOP + } + textIndex++; + byte[] profile = new byte[length - textIndex]; + System.arraycopy(chunk.getData(), textIndex, profile, 0, length - textIndex); + ByteArrayInputStream bais = new ByteArrayInputStream(profile); + InflaterInputStream iis = new InflaterInputStream(bais, new Inflater()); + try { + iccProfile = ICC_Profile.getInstance(iis); + } catch (IOException ioe) { + // this is OK; the profile will be null + } + } + + private void parse_sRGB_chunk(PNGChunk chunk) { + sRGBRenderingIntent = chunk.getByte(0); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/impl/PreloaderBMP.java b/src/main/java/org/apache/xmlgraphics/image/loader/impl/PreloaderBMP.java new file mode 100644 index 0000000..ef6545a --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/impl/PreloaderBMP.java @@ -0,0 +1,103 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PreloaderBMP.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.image.loader.impl; + +import java.io.IOException; +import java.nio.ByteOrder; + +import javax.imageio.stream.ImageInputStream; +import javax.xml.transform.Source; + +import org.apache.xmlgraphics.image.loader.ImageContext; +import org.apache.xmlgraphics.image.loader.ImageException; +import org.apache.xmlgraphics.image.loader.ImageInfo; +import org.apache.xmlgraphics.image.loader.ImageSize; +import org.apache.xmlgraphics.image.loader.util.ImageUtil; +import org.apache.xmlgraphics.util.UnitConv; + +/** + * Image preloader for BMP images. + */ +public class PreloaderBMP extends AbstractImagePreloader { + + /** Length of the BMP header */ + protected static final int BMP_SIG_LENGTH = 2; + + /** offset to width */ + private static final int WIDTH_OFFSET = 18; + + /** {@inheritDoc} */ + public ImageInfo preloadImage(String uri, Source src, ImageContext context) + throws IOException, ImageException { + if (!ImageUtil.hasImageInputStream(src)) { + return null; + } + ImageInputStream in = ImageUtil.needImageInputStream(src); + byte[] header = getHeader(in, BMP_SIG_LENGTH); + boolean supported = ((header[0] == (byte) 0x42) + && (header[1] == (byte) 0x4d)); + + if (supported) { + ImageInfo info = new ImageInfo(uri, "image/bmp"); + info.setSize(determineSize(in, context)); + return info; + } else { + return null; + } + } + + private ImageSize determineSize(ImageInputStream in, ImageContext context) + throws IOException, ImageException { + in.mark(); + ByteOrder oldByteOrder = in.getByteOrder(); + try { + ImageSize size = new ImageSize(); + + // BMP uses little endian notation! + in.setByteOrder(ByteOrder.LITTLE_ENDIAN); + + in.skipBytes(WIDTH_OFFSET); + int width = in.readInt(); + int height = in.readInt(); + size.setSizeInPixels(width, height); + + in.skipBytes(12); + int xRes = in.readInt(); + double xResDPI = UnitConv.in2mm(xRes / 1000d); + if (xResDPI == 0) { + xResDPI = context.getSourceResolution(); + } + + int yRes = in.readInt(); + double yResDPI = UnitConv.in2mm(yRes / 1000d); + if (yResDPI == 0) { + yResDPI = context.getSourceResolution(); + } + + size.setResolution(xResDPI, yResDPI); + size.calcSizeFromPixels(); + return size; + } finally { + in.setByteOrder(oldByteOrder); + in.reset(); + } + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/impl/PreloaderEMF.java b/src/main/java/org/apache/xmlgraphics/image/loader/impl/PreloaderEMF.java new file mode 100644 index 0000000..2d17dbb --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/impl/PreloaderEMF.java @@ -0,0 +1,117 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PreloaderEMF.java 1576437 2014-03-11 17:49:35Z vhennebert $ */ + +package org.apache.xmlgraphics.image.loader.impl; + +import java.io.IOException; +import java.nio.ByteOrder; + +import javax.imageio.stream.ImageInputStream; +import javax.xml.transform.Source; + +import org.apache.xmlgraphics.image.loader.ImageContext; +import org.apache.xmlgraphics.image.loader.ImageException; +import org.apache.xmlgraphics.image.loader.ImageInfo; +import org.apache.xmlgraphics.image.loader.ImageSize; +import org.apache.xmlgraphics.image.loader.util.ImageUtil; +import org.apache.xmlgraphics.util.UnitConv; + +/** + * Image preloader for EMF images. + */ +public class PreloaderEMF extends AbstractImagePreloader { + + /** Length of the EMF header */ + protected static final int EMF_SIG_LENGTH = 88; + + /** offset to signature */ + private static final int SIGNATURE_OFFSET = 40; + /** offset to width */ + private static final int WIDTH_OFFSET = 32; + /** offset to height */ + private static final int HEIGHT_OFFSET = 36; + /** offset to horizontal resolution in pixel */ + private static final int HRES_PIXEL_OFFSET = 72; + /** offset to vertical resolution in pixel */ + private static final int VRES_PIXEL_OFFSET = 76; + /** offset to horizontal resolution in mm */ + private static final int HRES_MM_OFFSET = 80; + /** offset to vertical resolution in mm */ + private static final int VRES_MM_OFFSET = 84; + + /** {@inheritDoc} */ + public ImageInfo preloadImage(String uri, Source src, ImageContext context) + throws IOException, ImageException { + if (!ImageUtil.hasImageInputStream(src)) { + return null; + } + ImageInputStream in = ImageUtil.needImageInputStream(src); + byte[] header = getHeader(in, EMF_SIG_LENGTH); + boolean supported + = ((header[SIGNATURE_OFFSET + 0] == (byte) 0x20) + && (header[SIGNATURE_OFFSET + 1] == (byte) 0x45) + && (header[SIGNATURE_OFFSET + 2] == (byte) 0x4D) + && (header[SIGNATURE_OFFSET + 3] == (byte) 0x46)); + + if (supported) { + ImageInfo info = new ImageInfo(uri, "image/emf"); + info.setSize(determineSize(in, context)); + return info; + } else { + return null; + } + } + + private ImageSize determineSize(ImageInputStream in, ImageContext context) + throws IOException, ImageException { + in.mark(); + ByteOrder oldByteOrder = in.getByteOrder(); + try { + ImageSize size = new ImageSize(); + + // BMP uses little endian notation! + in.setByteOrder(ByteOrder.LITTLE_ENDIAN); + + //resolution + in.skipBytes(WIDTH_OFFSET); + int width = (int)in.readUnsignedInt(); + int height = (int)in.readUnsignedInt(); + + in.skipBytes(HRES_PIXEL_OFFSET - WIDTH_OFFSET - 8); + long hresPixel = in.readUnsignedInt(); + long vresPixel = in.readUnsignedInt(); + long hresMM = in.readUnsignedInt(); + long vresMM = in.readUnsignedInt(); + double resHorz = hresPixel / UnitConv.mm2in(hresMM); + double resVert = vresPixel / UnitConv.mm2in(vresMM); + size.setResolution(resHorz, resVert); + + width = (int)Math.round(UnitConv.mm2mpt(width / 100f)); + height = (int)Math.round(UnitConv.mm2mpt(height / 100f)); + size.setSizeInMillipoints(width, height); + size.calcPixelsFromSize(); + + return size; + } finally { + in.setByteOrder(oldByteOrder); + in.reset(); + } + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/impl/PreloaderEPS.java b/src/main/java/org/apache/xmlgraphics/image/loader/impl/PreloaderEPS.java new file mode 100644 index 0000000..47b07e6 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/impl/PreloaderEPS.java @@ -0,0 +1,254 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PreloaderEPS.java 1610846 2014-07-15 20:44:18Z vhennebert $ */ + +package org.apache.xmlgraphics.image.loader.impl; + +import java.awt.geom.Rectangle2D; +import java.io.IOException; +import java.nio.ByteOrder; + +import javax.imageio.stream.ImageInputStream; +import javax.xml.transform.Source; + +import org.apache.xmlgraphics.image.loader.ImageContext; +import org.apache.xmlgraphics.image.loader.ImageInfo; +import org.apache.xmlgraphics.image.loader.ImageSize; +import org.apache.xmlgraphics.image.loader.util.ImageInputStreamAdapter; +import org.apache.xmlgraphics.image.loader.util.ImageUtil; +import org.apache.xmlgraphics.ps.DSCConstants; +import org.apache.xmlgraphics.ps.dsc.DSCException; +import org.apache.xmlgraphics.ps.dsc.DSCParser; +import org.apache.xmlgraphics.ps.dsc.DSCParserConstants; +import org.apache.xmlgraphics.ps.dsc.events.DSCComment; +import org.apache.xmlgraphics.ps.dsc.events.DSCCommentBoundingBox; +import org.apache.xmlgraphics.ps.dsc.events.DSCEvent; +import org.apache.xmlgraphics.util.MimeConstants; + +/** + * Image preloader for EPS images (Encapsulated PostScript). + */ +public class PreloaderEPS extends AbstractImagePreloader { + + /** Key for binary header object used in custom objects of the ImageInfo class. */ + public static final Object EPS_BINARY_HEADER = EPSBinaryFileHeader.class; + /** Key for bounding box used in custom objects of the ImageInfo class. */ + public static final Object EPS_BOUNDING_BOX = Rectangle2D.class; + + /** {@inheritDoc} */ + public ImageInfo preloadImage(String uri, Source src, ImageContext context) + throws IOException { + if (!ImageUtil.hasImageInputStream(src)) { + return null; + } + ImageInputStream in = ImageUtil.needImageInputStream(src); + in.mark(); + ByteOrder originalByteOrder = in.getByteOrder(); + in.setByteOrder(ByteOrder.LITTLE_ENDIAN); + EPSBinaryFileHeader binaryHeader = null; + try { + long magic = in.readUnsignedInt(); + magic &= 0xFFFFFFFFL; //Work-around for bug in Java 1.4.2 + // Check if binary header + boolean supported = false; + if (magic == 0xC6D3D0C5L) { + supported = true; //binary EPS + + binaryHeader = readBinaryFileHeader(in); + in.reset(); + in.mark(); //Mark start of file again + in.seek(binaryHeader.psStart); + + } else if (magic == 0x53502125L) { //"%!PS" in little endian + supported = true; //ascii EPS + in.reset(); + in.mark(); //Mark start of file again + } else { + in.reset(); + } + + if (supported) { + ImageInfo info = new ImageInfo(uri, MimeConstants.MIME_EPS); + boolean success = determineSize(in, context, info); + in.reset(); //Need to go back to start of file + if (!success) { + //No BoundingBox found, so probably no EPS + return null; + } + if (in.getStreamPosition() != 0) { + throw new IllegalStateException("Need to be at the start of the file here"); + } + if (binaryHeader != null) { + info.getCustomObjects().put(EPS_BINARY_HEADER, binaryHeader); + } + return info; + } else { + return null; + } + } finally { + in.setByteOrder(originalByteOrder); + } + } + + private EPSBinaryFileHeader readBinaryFileHeader(ImageInputStream in) throws IOException { + EPSBinaryFileHeader offsets = new EPSBinaryFileHeader(); + offsets.psStart = in.readUnsignedInt(); + offsets.psLength = in.readUnsignedInt(); + offsets.wmfStart = in.readUnsignedInt(); + offsets.wmfLength = in.readUnsignedInt(); + offsets.tiffStart = in.readUnsignedInt(); + offsets.tiffLength = in.readUnsignedInt(); + return offsets; + } + + private boolean determineSize(ImageInputStream in, ImageContext context, ImageInfo info) + throws IOException { + + in.mark(); + try { + Rectangle2D bbox = null; + DSCParser parser; + try { + parser = new DSCParser(new ImageInputStreamAdapter(in)); + outerLoop: + while (parser.hasNext()) { + DSCEvent event = parser.nextEvent(); + switch (event.getEventType()) { + case DSCParserConstants.HEADER_COMMENT: + case DSCParserConstants.COMMENT: + //ignore + break; + case DSCParserConstants.DSC_COMMENT: + DSCComment comment = event.asDSCComment(); + if (comment instanceof DSCCommentBoundingBox) { + DSCCommentBoundingBox bboxComment = (DSCCommentBoundingBox)comment; + if (DSCConstants.BBOX.equals(bboxComment.getName()) && bbox == null) { + bbox = (Rectangle2D)bboxComment.getBoundingBox().clone(); + //BoundingBox is good but HiRes is better so continue + } else if (DSCConstants.HIRES_BBOX.equals(bboxComment.getName())) { + bbox = (Rectangle2D)bboxComment.getBoundingBox().clone(); + //HiRefBBox is great so stop + break outerLoop; + } + } + break; + default: + //No more header so stop + break outerLoop; + } + } + if (bbox == null) { + return false; + } + } catch (DSCException e) { + throw new IOException("Error while parsing EPS file: " + e.getMessage()); + } + + ImageSize size = new ImageSize(); + size.setSizeInMillipoints( + (int)Math.round(bbox.getWidth() * 1000), + (int)Math.round(bbox.getHeight() * 1000)); + size.setResolution(context.getSourceResolution()); + size.calcPixelsFromSize(); + info.setSize(size); + info.getCustomObjects().put(EPS_BOUNDING_BOX, bbox); + return true; + } finally { + in.reset(); + } + } + + /** + * Holder class for various pointers to the contents of the EPS file. + */ + public static class EPSBinaryFileHeader { + + private long psStart; + private long psLength; + private long wmfStart; + private long wmfLength; + private long tiffStart; + private long tiffLength; + + /** + * Returns the start offset of the PostScript section. + * @return the start offset + */ + public long getPSStart() { + return psStart; + } + + /** + * Returns the length of the PostScript section. + * @return the length of the PostScript section (in bytes) + */ + public long getPSLength() { + return psLength; + } + + /** + * Indicates whether the EPS has a WMF preview. + * @return true if there is a WMF preview + */ + public boolean hasWMFPreview() { + return (wmfStart != 0); + } + + /** + * Returns the start offset of the WMF preview. + * @return the start offset (or 0 if there's no WMF preview) + */ + public long getWMFStart() { + return wmfStart; + } + + /** + * Returns the length of the WMF preview. + * @return the length of the WMF preview (in bytes) + */ + public long getWMFLength() { + return wmfLength; + } + + /** + * Indicates whether the EPS has a TIFF preview. + * @return true if there is a TIFF preview + */ + public boolean hasTIFFPreview() { + return (tiffStart != 0); + } + + /** + * Returns the start offset of the TIFF preview. + * @return the start offset (or 0 if there's no TIFF preview) + */ + public long getTIFFStart() { + return tiffStart; + } + + /** + * Returns the length of the TIFF preview. + * @return the length of the TIFF preview (in bytes) + */ + public long getTIFFLength() { + return tiffLength; + } + + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/impl/PreloaderGIF.java b/src/main/java/org/apache/xmlgraphics/image/loader/impl/PreloaderGIF.java new file mode 100644 index 0000000..0c1837d --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/impl/PreloaderGIF.java @@ -0,0 +1,86 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PreloaderGIF.java 1556077 2014-01-07 00:07:33Z lbernardo $ */ + +package org.apache.xmlgraphics.image.loader.impl; + +import java.io.IOException; +import java.util.Iterator; + +import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.stream.ImageInputStream; +import javax.xml.transform.Source; + +import org.apache.xmlgraphics.image.loader.ImageContext; +import org.apache.xmlgraphics.image.loader.ImageInfo; +import org.apache.xmlgraphics.image.loader.ImageSize; +import org.apache.xmlgraphics.image.loader.util.ImageUtil; +import org.apache.xmlgraphics.util.MimeConstants; + +/** + * Image preloader for GIF images. + */ +public class PreloaderGIF extends AbstractImagePreloader { + + private static final int GIF_SIG_LENGTH = 10; + + /** {@inheritDoc} */ + public ImageInfo preloadImage(String uri, Source src, ImageContext context) + throws IOException { + if (!ImageUtil.hasImageInputStream(src)) { + return null; + } + ImageInputStream in = ImageUtil.needImageInputStream(src); + byte[] header = getHeader(in, GIF_SIG_LENGTH); + boolean supported = ((header[0] == 'G') + && (header[1] == 'I') + && (header[2] == 'F') + && (header[3] == '8') + && (header[4] == '7' || header[4] == '9') + && (header[5] == 'a')); + + if (supported) { + ImageInfo info = new ImageInfo(uri, MimeConstants.MIME_GIF); + info.setSize(determineSize(header, context, in)); + return info; + } else { + return null; + } + } + + private ImageSize determineSize(byte[] header, ImageContext context, ImageInputStream in) throws IOException { + int [] dim = extractImageMetadata(in); + ImageSize size = new ImageSize(dim[0], dim[1], context.getSourceResolution()); + size.calcSizeFromPixels(); + return size; + } + + private int[] extractImageMetadata(ImageInputStream in) throws IOException { + long startPos = in.getStreamPosition(); + Iterator readers = ImageIO.getImageReadersByFormatName("gif"); + ImageReader reader = (ImageReader) readers.next(); + reader.setInput(in, true); + int width = reader.getWidth(0); + int height = reader.getHeight(0); + int[] dim = {width, height}; + in.seek(startPos); + return dim; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/impl/PreloaderJPEG.java b/src/main/java/org/apache/xmlgraphics/image/loader/impl/PreloaderJPEG.java new file mode 100644 index 0000000..7b64751 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/impl/PreloaderJPEG.java @@ -0,0 +1,251 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PreloaderJPEG.java 1681108 2015-05-22 13:26:12Z ssteiner $ */ + +package org.apache.xmlgraphics.image.loader.impl; + +import java.io.IOException; +import java.nio.ByteOrder; + +import javax.imageio.stream.ImageInputStream; +import javax.xml.transform.Source; + +import org.apache.xmlgraphics.image.loader.ImageContext; +import org.apache.xmlgraphics.image.loader.ImageException; +import org.apache.xmlgraphics.image.loader.ImageInfo; +import org.apache.xmlgraphics.image.loader.ImageSize; +import org.apache.xmlgraphics.image.loader.util.ImageUtil; +import org.apache.xmlgraphics.util.MimeConstants; +import org.apache.xmlgraphics.util.UnitConv; + +/** + * Image preloader for JPEG images. + */ +public class PreloaderJPEG extends AbstractImagePreloader implements JPEGConstants { + + private static final int JPG_SIG_LENGTH = 3; + private static final int[] BYTES_PER_COMPONENT = {0, 1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8}; // ignore 0 + private static final int EXIF = 0x45786966; + private static final int II = 0x4949; // Intel + private static final int MM = 0x4d4d; // Motorola + private static final int X_RESOLUTION = 0x011a; + private static final int Y_RESOLUTION = 0x011b; + private static final int RESOLUTION_UNIT = 0x0128; + + /** {@inheritDoc} + * @throws ImageException */ + public ImageInfo preloadImage(String uri, Source src, ImageContext context) + throws IOException, ImageException { + if (!ImageUtil.hasImageInputStream(src)) { + return null; + } + ImageInputStream in = ImageUtil.needImageInputStream(src); + byte[] header = getHeader(in, JPG_SIG_LENGTH); + boolean supported = ((header[0] == (byte)MARK) + && (header[1] == (byte)SOI) + && (header[2] == (byte)MARK)); + + if (supported) { + ImageInfo info = new ImageInfo(uri, MimeConstants.MIME_JPEG); + info.setSize(determineSize(in, context)); + return info; + } else { + return null; + } + } + + private ImageSize determineSize(ImageInputStream in, ImageContext context) + throws IOException, ImageException { + in.mark(); + try { + ImageSize size = new ImageSize(); + JPEGFile jpeg = new JPEGFile(in); + + while (true) { + int segID = jpeg.readMarkerSegment(); + //System.out.println("Segment: " + Integer.toHexString(segID)); + switch (segID) { + case SOI: + case NULL: + break; + case APP0: + int reclen = jpeg.readSegmentLength(); + in.skipBytes(7); + int densityUnits = in.read(); + int xdensity = in.readUnsignedShort(); + int ydensity = in.readUnsignedShort(); + if (size.getDpiHorizontal() == 0) { + if (densityUnits == 2) { + //dots per centimeter + size.setResolution( + xdensity * UnitConv.IN2CM, + ydensity * UnitConv.IN2CM); + } else if (densityUnits == 1) { + //dots per inch + size.setResolution(xdensity, ydensity); + } else { + //resolution not specified + size.setResolution(context.getSourceResolution()); + } + } + if (size.getWidthPx() != 0) { + size.calcSizeFromPixels(); + return size; + } + in.skipBytes(reclen - 14); + break; + case APP1: + // see http://www.media.mit.edu/pia/Research/deepview/exif.html + reclen = jpeg.readSegmentLength(); + int bytesToEnd = reclen - 2; + // read Exif Header: 0x.45.78.69.66.00.00 + int exif = in.readInt(); // 0x.45.78.69.66 + in.readUnsignedShort(); // 0x.00.00 + // in.skipBytes(6); + bytesToEnd -= 6; + if (exif != EXIF) { + // there may be multiple APP1 segments but we want the Exif one + in.skipBytes(bytesToEnd); + break; + } + // start TIFF data + int currentTIFFOffset = 0; + // byte align: 0x.49.49 (19789) means Intel, 0x.4D.4D means Motorola + int align = in.readUnsignedShort(); + bytesToEnd -= 2; + currentTIFFOffset += 2; + ByteOrder originalByteOrder = in.getByteOrder(); + // Intel = little, Motorola = big + in.setByteOrder(align == MM ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN); + in.skipBytes(2); // 0x.2A.00 (Intel) or 0x.00.2A (Motorola) + bytesToEnd -= 2; + currentTIFFOffset += 2; + int firstIFDOffset = in.readInt(); + bytesToEnd -= 4; + currentTIFFOffset += 4; + in.skipBytes(firstIFDOffset - 8); + bytesToEnd -= firstIFDOffset - 8; + currentTIFFOffset += firstIFDOffset - 8; + int directoryEntries = in.readUnsignedShort(); + bytesToEnd -= 2; + currentTIFFOffset += 2; + int resolutionOffset = 0; + int resolutionFormat = 0; + int resolutionUnits = 0; + int resolution = 0; + boolean foundResolution = false; + for (int j = 0; j < directoryEntries; j++) { + int tag = in.readUnsignedShort(); + if ((tag == X_RESOLUTION || tag == Y_RESOLUTION) && !foundResolution) { + // 0x011A (XResolution), 0x011B (YResolution) + int format = in.readUnsignedShort(); + int components = in.readInt(); + int dataByteLength = components * BYTES_PER_COMPONENT[format]; + int value = in.readInt(); + if (dataByteLength > 4) { + // value is offset to data value + resolutionOffset = value; + } else { + // value is data value + resolution = value; + } + resolutionFormat = format; + foundResolution = true; + } else if (tag == RESOLUTION_UNIT) { + // 0x0128 (ResolutionUnit) + int format = in.readUnsignedShort(); + int components = in.readInt(); + int dataByteLength = components * BYTES_PER_COMPONENT[format]; + if (dataByteLength < 5 && format == 3) { + int value = in.readUnsignedShort(); + in.skipBytes(2); + resolutionUnits = value; + } else { + in.skipBytes(4); + } + } else { + in.skipBytes(10); + } + bytesToEnd -= 12; + currentTIFFOffset += 12; + } + in.readInt(); // not needed, but has thumbnail info + bytesToEnd -= 4; + currentTIFFOffset += 4; + if (resolutionOffset != 0) { + in.skipBytes(resolutionOffset - currentTIFFOffset); + bytesToEnd -= resolutionOffset - currentTIFFOffset; + if (resolutionFormat == 5 || resolutionFormat == 10) { + int numerator = in.readInt(); + int denominator = in.readInt(); + resolution = numerator / denominator; + bytesToEnd -= 8; + } + } + in.skipBytes(bytesToEnd); + in.setByteOrder(originalByteOrder); + if (resolutionUnits == 3) { + // dots per centimeter + size.setResolution(resolution * UnitConv.IN2CM, resolution * UnitConv.IN2CM); + } else if (resolutionUnits == 2) { + // dots per inch + size.setResolution(resolution, resolution); + } else { + // resolution not specified; set default if not set yet + if (size.getDpiHorizontal() == 0) { + size.setResolution(context.getSourceResolution()); + } + } + if (size.getWidthPx() != 0) { + size.calcSizeFromPixels(); + return size; + } + break; + case SOF0: + case SOF1: + case SOF2: // SOF2 and SOFA are only supported by PDF 1.3 + case SOFA: + reclen = jpeg.readSegmentLength(); + in.skipBytes(1); + int height = in.readUnsignedShort(); + int width = in.readUnsignedShort(); + size.setSizeInPixels(width, height); + if (size.getDpiHorizontal() != 0) { + size.calcSizeFromPixels(); + return size; + } + in.skipBytes(reclen - 7); + break; + case SOS: + case EOI: + // Break as early as possible (we don't want to read the whole file here) + if (size.getDpiHorizontal() == 0) { + size.setResolution(context.getSourceResolution()); + size.calcSizeFromPixels(); + } + return size; + default: + jpeg.skipCurrentMarkerSegment(); + } + } + } finally { + in.reset(); + } + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/impl/PreloaderRawPNG.java b/src/main/java/org/apache/xmlgraphics/image/loader/impl/PreloaderRawPNG.java new file mode 100644 index 0000000..8f1b08a --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/impl/PreloaderRawPNG.java @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PreloaderRawPNG.java 1732018 2016-02-24 04:51:06Z gadams $ */ +package org.apache.xmlgraphics.image.loader.impl; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import javax.imageio.stream.ImageInputStream; +import javax.xml.transform.Source; + +import org.apache.xmlgraphics.image.codec.png.PNGImageDecoder; +import org.apache.xmlgraphics.image.loader.ImageContext; +import org.apache.xmlgraphics.image.loader.ImageException; +import org.apache.xmlgraphics.image.loader.ImageInfo; +import org.apache.xmlgraphics.image.loader.ImageSize; +import org.apache.xmlgraphics.image.loader.util.ImageUtil; + +public class PreloaderRawPNG extends AbstractImagePreloader { + public ImageInfo preloadImage(String uri, Source src, ImageContext context) throws ImageException, IOException { + if (!ImageUtil.hasImageInputStream(src)) { + return null; + } + ImageInputStream in = ImageUtil.needImageInputStream(src); + long bb = ByteBuffer.wrap(getHeader(in, 8)).getLong(); + if (bb != PNGConstants.PNG_SIGNATURE) { + return null; + } + in.mark(); + ImageSize size = new ImageSize(); + //Resolution (first a default, then try to read the metadata) + size.setResolution(context.getSourceResolution()); + try { + PNGImageDecoder.readPNGHeader(in, size); + } finally { + in.reset(); + } + + ImageInfo info = new ImageInfo(uri, "image/png"); + info.setSize(size); + return info; + } + + public int getPriority() { + return DEFAULT_PRIORITY * 2; + } +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/impl/PreloaderTIFF.java b/src/main/java/org/apache/xmlgraphics/image/loader/impl/PreloaderTIFF.java new file mode 100644 index 0000000..b91d45e --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/impl/PreloaderTIFF.java @@ -0,0 +1,200 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PreloaderTIFF.java 1681108 2015-05-22 13:26:12Z ssteiner $ */ + +package org.apache.xmlgraphics.image.loader.impl; + +import java.io.IOException; +import java.text.MessageFormat; + +import javax.imageio.stream.ImageInputStream; +import javax.xml.transform.Source; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.apache.xmlgraphics.image.codec.tiff.TIFFDirectory; +import org.apache.xmlgraphics.image.codec.tiff.TIFFField; +import org.apache.xmlgraphics.image.codec.tiff.TIFFImageDecoder; +import org.apache.xmlgraphics.image.codec.util.SeekableStream; +import org.apache.xmlgraphics.image.loader.ImageContext; +import org.apache.xmlgraphics.image.loader.ImageException; +import org.apache.xmlgraphics.image.loader.ImageInfo; +import org.apache.xmlgraphics.image.loader.ImageSize; +import org.apache.xmlgraphics.image.loader.SubImageNotFoundException; +import org.apache.xmlgraphics.image.loader.util.ImageUtil; +import org.apache.xmlgraphics.image.loader.util.SeekableStreamAdapter; +import org.apache.xmlgraphics.util.MimeConstants; +import org.apache.xmlgraphics.util.UnitConv; + +/** + * Image preloader for TIFF images. + *

    + * Note: The implementation relies on the TIFF codec code in Apache XML Graphics Commons for + * access to the TIFF directory. + */ +public class PreloaderTIFF extends AbstractImagePreloader { + + private static Log log = LogFactory.getLog(PreloaderTIFF.class); + + private static final int TIFF_SIG_LENGTH = 8; + + /** {@inheritDoc} + * @throws ImageException */ + public ImageInfo preloadImage(String uri, Source src, ImageContext context) + throws IOException, ImageException { + if (!ImageUtil.hasImageInputStream(src)) { + return null; + } + ImageInputStream in = ImageUtil.needImageInputStream(src); + byte[] header = getHeader(in, TIFF_SIG_LENGTH); + boolean supported = false; + + // first 2 bytes = II (little endian encoding) + if (header[0] == (byte) 0x49 && header[1] == (byte) 0x49) { + + // look for '42' in byte 3 and '0' in byte 4 + if (header[2] == 42 && header[3] == 0) { + supported = true; + } + } + + // first 2 bytes == MM (big endian encoding) + if (header[0] == (byte) 0x4D && header[1] == (byte) 0x4D) { + + // look for '42' in byte 4 and '0' in byte 3 + if (header[2] == 0 && header[3] == 42) { + supported = true; + } + } + + if (supported) { + ImageInfo info = createImageInfo(uri, in, context); + return info; + } else { + return null; + } + } + + private ImageInfo createImageInfo(String uri, ImageInputStream in, ImageContext context) + throws IOException, ImageException { + ImageInfo info = null; + in.mark(); + try { + int pageIndex = ImageUtil.needPageIndexFromURI(uri); + + SeekableStream seekable = new SeekableStreamAdapter(in); + TIFFDirectory dir; + try { + dir = new TIFFDirectory(seekable, pageIndex); + } catch (IllegalArgumentException iae) { + String errorMessage = MessageFormat.format( + "Subimage {0} does not exist.", new Object[] {pageIndex}); + throw new SubImageNotFoundException(errorMessage); + } + int width = (int)dir.getFieldAsLong(TIFFImageDecoder.TIFF_IMAGE_WIDTH); + int height = (int)dir.getFieldAsLong(TIFFImageDecoder.TIFF_IMAGE_LENGTH); + ImageSize size = new ImageSize(); + size.setSizeInPixels(width, height); + int unit = 2; //inch is default + if (dir.isTagPresent(TIFFImageDecoder.TIFF_RESOLUTION_UNIT)) { + unit = (int)dir.getFieldAsLong(TIFFImageDecoder.TIFF_RESOLUTION_UNIT); + } + if (unit == 2 || unit == 3) { + float xRes; + float yRes; + TIFFField fldx = dir.getField(TIFFImageDecoder.TIFF_X_RESOLUTION); + TIFFField fldy = dir.getField(TIFFImageDecoder.TIFF_Y_RESOLUTION); + if (fldx == null || fldy == null) { + unit = 2; + xRes = context.getSourceResolution(); + yRes = xRes; + } else { + xRes = fldx.getAsFloat(0); + yRes = fldy.getAsFloat(0); + } + if (xRes == 0 || yRes == 0) { + //Some TIFFs may report 0 here which would lead to problems + size.setResolution(context.getSourceResolution()); + } else if (unit == 2) { + size.setResolution(xRes, yRes); //Inch + } else { + size.setResolution( + UnitConv.in2mm(xRes) / 10, + UnitConv.in2mm(yRes) / 10); //Centimeters + } + } else { + size.setResolution(context.getSourceResolution()); + } + size.calcSizeFromPixels(); + if (log.isTraceEnabled()) { + log.trace("TIFF image detected: " + size); + } + + info = new ImageInfo(uri, MimeConstants.MIME_TIFF); + info.setSize(size); + + TIFFField fld; + + fld = dir.getField(TIFFImageDecoder.TIFF_COMPRESSION); + if (fld != null) { + int compression = fld.getAsInt(0); + if (log.isTraceEnabled()) { + log.trace("TIFF compression: " + compression); + } + info.getCustomObjects().put("TIFF_COMPRESSION", compression); + } + + fld = dir.getField(TIFFImageDecoder.TIFF_TILE_WIDTH); + if (fld != null) { + if (log.isTraceEnabled()) { + log.trace("TIFF is tiled"); + } + info.getCustomObjects().put("TIFF_TILED", Boolean.TRUE); + } + + int stripCount; + fld = dir.getField(TIFFImageDecoder.TIFF_ROWS_PER_STRIP); + if (fld == null) { + stripCount = 1; + } else { + stripCount = (int)Math.ceil(size.getHeightPx() / (double)fld.getAsLong(0)); + } + if (log.isTraceEnabled()) { + log.trace("TIFF has " + stripCount + " strips."); + } + info.getCustomObjects().put("TIFF_STRIP_COUNT", stripCount); + + try { + //Check if there is a next page + new TIFFDirectory(seekable, pageIndex + 1); + info.getCustomObjects().put(ImageInfo.HAS_MORE_IMAGES, Boolean.TRUE); + if (log.isTraceEnabled()) { + log.trace("TIFF is multi-page."); + } + } catch (IllegalArgumentException iae) { + info.getCustomObjects().put(ImageInfo.HAS_MORE_IMAGES, Boolean.FALSE); + } + } finally { + in.reset(); + } + + return info; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/impl/imageio/ImageIOUtil.java b/src/main/java/org/apache/xmlgraphics/image/loader/impl/imageio/ImageIOUtil.java new file mode 100644 index 0000000..bde0992 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/impl/imageio/ImageIOUtil.java @@ -0,0 +1,133 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageIOUtil.java 1804124 2017-08-04 14:13:54Z ssteiner $ */ + +package org.apache.xmlgraphics.image.loader.impl.imageio; + +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.metadata.IIOMetadataFormatImpl; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Result; +import javax.xml.transform.Source; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerConfigurationException; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import org.apache.xmlgraphics.image.loader.ImageSize; +import org.apache.xmlgraphics.util.UnitConv; + +/** + * Helper and convenience methods for ImageIO. + */ +public final class ImageIOUtil { + + private ImageIOUtil() { + } + + /** Key for ImageInfo's custom objects to embed the ImageIO metadata */ + public static final Object IMAGEIO_METADATA = IIOMetadata.class; + + /** + * Extracts the resolution information from the standard ImageIO metadata. + * @param iiometa the metadata provided by ImageIO + * @param size the image size object + */ + public static void extractResolution(IIOMetadata iiometa, ImageSize size) { + if (iiometa != null && iiometa.isStandardMetadataFormatSupported()) { + Element metanode = (Element)iiometa.getAsTree( + IIOMetadataFormatImpl.standardMetadataFormatName); + Element dim = getChild(metanode, "Dimension"); + if (dim != null) { + Element child; + double dpiHorz = size.getDpiHorizontal(); + double dpiVert = size.getDpiVertical(); + child = getChild(dim, "HorizontalPixelSize"); + if (child != null) { + float value = Float.parseFloat(child.getAttribute("value")); + if (value != 0 && !Float.isInfinite(value)) { + dpiHorz = UnitConv.IN2MM / value; + } + } + child = getChild(dim, "VerticalPixelSize"); + if (child != null) { + float value = Float.parseFloat(child.getAttribute("value")); + if (value != 0 && !Float.isInfinite(value)) { + dpiVert = UnitConv.IN2MM / value; + } + } + size.setResolution(dpiHorz, dpiVert); + size.calcSizeFromPixels(); + } + } + } + + /** + * Returns a child element of another element or null if there's no such child. + * @param el the parent element + * @param name the name of the requested child + * @return the child or null if there's no such child + */ + public static Element getChild(Element el, String name) { + NodeList nodes = el.getElementsByTagName(name); + if (nodes.getLength() > 0) { + return (Element)nodes.item(0); + } else { + return null; + } + } + + /** + * Dumps the content of an IIOMetadata instance to System.out. + * @param iiometa the metadata + */ + public static void dumpMetadataToSystemOut(IIOMetadata iiometa) { + String[] metanames = iiometa.getMetadataFormatNames(); + for (String metaname : metanames) { + System.out.println("--->" + metaname); + dumpNodeToSystemOut(iiometa.getAsTree(metaname)); + } + } + + /** + * Serializes a W3C DOM node to a String and dumps it to System.out. + * @param node a W3C DOM node + */ + private static void dumpNodeToSystemOut(Node node) { + Transformer trans = null; + try { + trans = TransformerFactory.newInstance().newTransformer(); + trans.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); + trans.setOutputProperty(OutputKeys.INDENT, "yes"); + Source src = new DOMSource(node); + Result res = new StreamResult(System.out); + trans.transform(src, res); + } catch (TransformerConfigurationException e) { + e.printStackTrace(); + } catch (TransformerException e) { + e.printStackTrace(); + } + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/impl/imageio/ImageLoaderFactoryImageIO.java b/src/main/java/org/apache/xmlgraphics/image/loader/impl/imageio/ImageLoaderFactoryImageIO.java new file mode 100644 index 0000000..6dc00a7 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/impl/imageio/ImageLoaderFactoryImageIO.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageLoaderFactoryImageIO.java 1681137 2015-05-22 14:54:05Z ssteiner $ */ + +package org.apache.xmlgraphics.image.loader.impl.imageio; + +import javax.imageio.ImageIO; + +import org.apache.xmlgraphics.image.loader.ImageFlavor; +import org.apache.xmlgraphics.image.loader.impl.AbstractImageLoaderFactory; +import org.apache.xmlgraphics.image.loader.spi.ImageLoader; + +/** + * Factory class for the ImageLoader based on ImageIO. + */ +public class ImageLoaderFactoryImageIO extends AbstractImageLoaderFactory { + + private static final ImageFlavor[] FLAVORS = new ImageFlavor[] { + ImageFlavor.RENDERED_IMAGE, + ImageFlavor.BUFFERED_IMAGE}; + + /** {@inheritDoc} */ + public String[] getSupportedMIMETypes() { + return ImageIO.getReaderMIMETypes(); + } + + /** {@inheritDoc} */ + public ImageFlavor[] getSupportedFlavors(String mime) { + return FLAVORS; + } + + /** {@inheritDoc} */ + public ImageLoader newImageLoader(ImageFlavor targetFlavor) { + return new ImageLoaderImageIO(targetFlavor); + } + + /** {@inheritDoc} */ + public boolean isAvailable() { + return (getSupportedMIMETypes().length > 0); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/impl/imageio/ImageLoaderImageIO.java b/src/main/java/org/apache/xmlgraphics/image/loader/impl/imageio/ImageLoaderImageIO.java new file mode 100644 index 0000000..7e78dd5 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/impl/imageio/ImageLoaderImageIO.java @@ -0,0 +1,417 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageLoaderImageIO.java 1804124 2017-08-04 14:13:54Z ssteiner $ */ + +package org.apache.xmlgraphics.image.loader.impl.imageio; + +import java.awt.Color; +import java.awt.color.ICC_ColorSpace; +import java.awt.color.ICC_Profile; +import java.awt.image.BufferedImage; +import java.awt.image.ColorModel; +import java.awt.image.ComponentColorModel; +import java.awt.image.IndexColorModel; +import java.awt.image.Raster; +import java.awt.image.RenderedImage; +import java.awt.image.WritableRaster; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.zip.DataFormatException; +import java.util.zip.Inflater; + +import javax.imageio.IIOException; +import javax.imageio.ImageIO; +import javax.imageio.ImageReadParam; +import javax.imageio.ImageReader; +import javax.imageio.ImageTypeSpecifier; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.metadata.IIOMetadataFormatImpl; +import javax.imageio.metadata.IIOMetadataNode; +import javax.imageio.spi.IIOServiceProvider; +import javax.imageio.stream.ImageInputStream; +import javax.xml.transform.Source; + +import org.w3c.dom.Element; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.apache.xmlgraphics.image.loader.Image; +import org.apache.xmlgraphics.image.loader.ImageException; +import org.apache.xmlgraphics.image.loader.ImageFlavor; +import org.apache.xmlgraphics.image.loader.ImageInfo; +import org.apache.xmlgraphics.image.loader.ImageSessionContext; +import org.apache.xmlgraphics.image.loader.impl.AbstractImageLoader; +import org.apache.xmlgraphics.image.loader.impl.ImageBuffered; +import org.apache.xmlgraphics.image.loader.impl.ImageRendered; +import org.apache.xmlgraphics.image.loader.util.ImageUtil; +import org.apache.xmlgraphics.io.XmlSourceUtil; +import org.apache.xmlgraphics.java2d.color.profile.ColorProfileUtil; + +/** + * An ImageLoader implementation based on ImageIO for loading bitmap images. + */ +public class ImageLoaderImageIO extends AbstractImageLoader { + + /** logger */ + protected static final Log log = LogFactory.getLog(ImageLoaderImageIO.class); + + private ImageFlavor targetFlavor; + + private static final String PNG_METADATA_NODE = "javax_imageio_png_1.0"; + + private static final String JPEG_METADATA_NODE = "javax_imageio_jpeg_image_1.0"; + + private static final Set PROVIDERS_IGNORING_ICC = new HashSet(); + + /** + * Main constructor. + * @param targetFlavor the target flavor + */ + public ImageLoaderImageIO(ImageFlavor targetFlavor) { + if (!(ImageFlavor.BUFFERED_IMAGE.equals(targetFlavor) + || ImageFlavor.RENDERED_IMAGE.equals(targetFlavor))) { + throw new IllegalArgumentException("Unsupported target ImageFlavor: " + targetFlavor); + } + this.targetFlavor = targetFlavor; + } + + /** {@inheritDoc} */ + public ImageFlavor getTargetFlavor() { + return this.targetFlavor; + } + + /** {@inheritDoc} */ + public Image loadImage(ImageInfo info, Map hints, ImageSessionContext session) + throws ImageException, IOException { + RenderedImage imageData = null; + IIOException firstException = null; + + IIOMetadata iiometa = (IIOMetadata)info.getCustomObjects().get( + ImageIOUtil.IMAGEIO_METADATA); + boolean ignoreMetadata = (iiometa != null); + boolean providerIgnoresICC = false; + + Source src = session.needSource(info.getOriginalURI()); + ImageInputStream imgStream = ImageUtil.needImageInputStream(src); + try { + Iterator iter = ImageIO.getImageReaders(imgStream); + while (iter.hasNext()) { + ImageReader reader = (ImageReader)iter.next(); + try { + imgStream.mark(); + reader.setInput(imgStream, false, ignoreMetadata); + ImageReadParam param = getParam(reader, hints); + final int pageIndex = ImageUtil.needPageIndexFromURI(info.getOriginalURI()); + try { +// if (ImageFlavor.BUFFERED_IMAGE.equals(this.targetFlavor)) { + imageData = reader.read(pageIndex, param); +// } else { +// imageData = reader.read(pageIndex, param); + //imageData = reader.readAsRenderedImage(pageIndex, param); + //TODO Reenable the above when proper listeners are implemented + //to react to late pixel population (so the stream can be closed + //properly). +// } + if (iiometa == null) { + iiometa = reader.getImageMetadata(pageIndex); + } + providerIgnoresICC = checkProviderIgnoresICC(reader + .getOriginatingProvider()); + break; //Quit early, we have the image + } catch (IndexOutOfBoundsException indexe) { + throw new ImageException("Page does not exist. Invalid image index: " + + pageIndex); + } catch (IllegalArgumentException iae) { + //Some codecs like com.sun.imageio.plugins.wbmp.WBMPImageReader throw + //IllegalArgumentExceptions when they have trouble parsing the image. + throw new ImageException("Error loading image using ImageIO codec", iae); + } catch (IIOException iioe) { + if (firstException == null) { + firstException = iioe; + } else { + log.debug("non-first error loading image: " + iioe.getMessage()); + } + } + try { + //Try fallback for CMYK images + BufferedImage bi = getFallbackBufferedImage(reader, pageIndex, param); + imageData = bi; + firstException = null; //Clear exception after successful fallback attempt + break; + } catch (IIOException iioe) { + //ignore + } + imgStream.reset(); + } finally { + reader.dispose(); + } + } + } finally { + XmlSourceUtil.closeQuietly(src); + //TODO Some codecs may do late reading. + } + if (firstException != null) { + throw new ImageException("Error while loading image: " + + firstException.getMessage(), firstException); + } + if (imageData == null) { + throw new ImageException("No ImageIO ImageReader found ."); + } + + ColorModel cm = imageData.getColorModel(); + + Color transparentColor = null; + if (cm instanceof IndexColorModel) { + //transparent color will be extracted later from the image + } else { + if (providerIgnoresICC && cm instanceof ComponentColorModel) { + // Apply ICC Profile to Image by creating a new image with a new + // color model. + ICC_Profile iccProf = tryToExctractICCProfile(iiometa); + if (iccProf != null) { + ColorModel cm2 = new ComponentColorModel( + new ICC_ColorSpace(iccProf), cm.hasAlpha(), cm + .isAlphaPremultiplied(), cm + .getTransparency(), cm.getTransferType()); + WritableRaster wr = Raster.createWritableRaster(imageData + .getSampleModel(), null); + imageData.copyData(wr); + try { + BufferedImage bi = new BufferedImage(cm2, wr, cm2 + .isAlphaPremultiplied(), null); + imageData = bi; + cm = cm2; + } catch (IllegalArgumentException iae) { + String msg = "Image " + info.getOriginalURI() + + " has an incompatible color profile." + + " The color profile will be ignored." + + "\nColor model of loaded bitmap: " + cm + + "\nColor model of color profile: " + cm2; + if (info.getCustomObjects().get("warningincustomobject") != null) { + info.getCustomObjects().put("warning", msg); + } else { + log.warn(msg); + } + } + } + } + + // ImageIOUtil.dumpMetadataToSystemOut(iiometa); + // Retrieve the transparent color from the metadata + if (iiometa != null && iiometa.isStandardMetadataFormatSupported()) { + Element metanode = (Element)iiometa.getAsTree( + IIOMetadataFormatImpl.standardMetadataFormatName); + Element dim = ImageIOUtil.getChild(metanode, "Transparency"); + if (dim != null) { + Element child; + child = ImageIOUtil.getChild(dim, "TransparentColor"); + if (child != null) { + String value = child.getAttribute("value"); + if (value.length() == 0) { + //ignore + } else if (cm.getNumColorComponents() == 1) { + int gray = Integer.parseInt(value); + transparentColor = new Color(gray, gray, gray); + } else { + StringTokenizer st = new StringTokenizer(value); + transparentColor = new Color( + Integer.parseInt(st.nextToken()), + Integer.parseInt(st.nextToken()), + Integer.parseInt(st.nextToken())); + } + } + } + } + } + + if (ImageFlavor.BUFFERED_IMAGE.equals(this.targetFlavor)) { + return new ImageBuffered(info, (BufferedImage)imageData, transparentColor); + } else { + return new ImageRendered(info, imageData, transparentColor); + } + } + + private ImageReadParam getParam(ImageReader reader, Map hints) throws IOException { + if (hints != null && Boolean.TRUE.equals(hints.get("CMYK"))) { + Iterator types = reader.getImageTypes(0); + while (types.hasNext()) { + ImageTypeSpecifier type = types.next(); + if (type.getNumComponents() == 4) { + ImageReadParam param = new ImageReadParam(); + param.setDestinationType(type); + return param; + } + } + } + return reader.getDefaultReadParam(); + } + + /** + * Checks if the provider ignores the ICC color profile. This method will + * assume providers work correctly, and return false if the provider is + * unknown. This ensures backward-compatibility. + * + * @param provider + * the ImageIO Provider + * @return true if we know the provider to be broken and ignore ICC + * profiles. + */ + private boolean checkProviderIgnoresICC(IIOServiceProvider provider) { + // TODO: This information could be cached. + StringBuffer b = new StringBuffer(provider.getDescription(Locale.ENGLISH)); + b.append('/').append(provider.getVendorName()); + b.append('/').append(provider.getVersion()); + if (log.isDebugEnabled()) { + log.debug("Image Provider: " + b.toString()); + } + return ImageLoaderImageIO.PROVIDERS_IGNORING_ICC.contains(b.toString()); + } + + /** + * Extract ICC Profile from ImageIO Metadata. This method currently only + * supports PNG and JPEG metadata. + * + * @param iiometa + * The ImageIO Metadata + * @return an ICC Profile or null. + */ + private ICC_Profile tryToExctractICCProfile(IIOMetadata iiometa) { + ICC_Profile iccProf = null; + String[] supportedFormats = iiometa.getMetadataFormatNames(); + for (String format : supportedFormats) { + Element root = (Element) iiometa.getAsTree(format); + if (PNG_METADATA_NODE.equals(format)) { + iccProf = this + .tryToExctractICCProfileFromPNGMetadataNode(root); + } else if (JPEG_METADATA_NODE.equals(format)) { + iccProf = this.tryToExctractICCProfileFromJPEGMetadataNode(root); + } + } + return iccProf; + } + + private ICC_Profile tryToExctractICCProfileFromPNGMetadataNode( + Element pngNode) { + ICC_Profile iccProf = null; + Element iccpNode = ImageIOUtil.getChild(pngNode, "iCCP"); + if (iccpNode instanceof IIOMetadataNode) { + IIOMetadataNode imn = (IIOMetadataNode) iccpNode; + byte[] prof = (byte[]) imn.getUserObject(); + String comp = imn.getAttribute("compressionMethod"); + if ("deflate".equalsIgnoreCase(comp)) { + Inflater decompresser = new Inflater(); + decompresser.setInput(prof); + byte[] result = new byte[100]; + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + boolean failed = false; + while (!decompresser.finished() && !failed) { + try { + int resultLength = decompresser.inflate(result); + bos.write(result, 0, resultLength); + if (resultLength == 0) { + // this means more data or an external dictionary is + // needed. Both of which are not available, so we + // fail. + log.debug("Failed to deflate ICC Profile"); + failed = true; + } + } catch (DataFormatException e) { + log.debug("Failed to deflate ICC Profile", e); + failed = true; + } + } + decompresser.end(); + try { + iccProf = ColorProfileUtil.getICC_Profile(bos.toByteArray()); + } catch (IllegalArgumentException e) { + log.debug("Failed to interpret embedded ICC Profile", e); + iccProf = null; + } + } + } + return iccProf; + } + + private ICC_Profile tryToExctractICCProfileFromJPEGMetadataNode( + Element jpgNode) { + ICC_Profile iccProf = null; + Element jfifNode = ImageIOUtil.getChild(jpgNode, "app0JFIF"); + if (jfifNode != null) { + Element app2iccNode = ImageIOUtil.getChild(jfifNode, "app2ICC"); + if (app2iccNode instanceof IIOMetadataNode) { + IIOMetadataNode imn = (IIOMetadataNode) app2iccNode; + iccProf = (ICC_Profile) imn.getUserObject(); + } + } + return iccProf; + } + + private BufferedImage getFallbackBufferedImage(ImageReader reader, + int pageIndex, ImageReadParam param) throws IOException { + //Work-around found at: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4799903 + //There are some additional ideas there if someone wants to go further. + + // Try reading a Raster (no color conversion). + Raster raster = reader.readRaster(pageIndex, param); + + // Arbitrarily select a BufferedImage type. + int imageType; + int numBands = raster.getNumBands(); + switch(numBands) { + case 1: + imageType = BufferedImage.TYPE_BYTE_GRAY; + break; + case 3: + imageType = BufferedImage.TYPE_3BYTE_BGR; + break; + case 4: + imageType = BufferedImage.TYPE_4BYTE_ABGR; + break; + default: + throw new UnsupportedOperationException("Unsupported band count: " + numBands); + } + + // Create a BufferedImage. + BufferedImage bi = new BufferedImage(raster.getWidth(), + raster.getHeight(), + imageType); + + // Set the image data. + bi.getRaster().setRect(raster); + return bi; + } + + static { + // TODO: This list could be kept in a resource file. + PROVIDERS_IGNORING_ICC + .add("Standard PNG image reader/Sun Microsystems, Inc./1.0"); + PROVIDERS_IGNORING_ICC + .add("Standard PNG image reader/Oracle Corporation/1.0"); + PROVIDERS_IGNORING_ICC + .add("Standard JPEG Image Reader/Sun Microsystems, Inc./0.5"); + PROVIDERS_IGNORING_ICC + .add("Standard JPEG Image Reader/Oracle Corporation/0.5"); + } +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/impl/imageio/PreloaderImageIO.java b/src/main/java/org/apache/xmlgraphics/image/loader/impl/imageio/PreloaderImageIO.java new file mode 100644 index 0000000..a42a7ea --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/impl/imageio/PreloaderImageIO.java @@ -0,0 +1,124 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PreloaderImageIO.java 1452610 2013-03-05 01:00:35Z lbernardo $ */ + +package org.apache.xmlgraphics.image.loader.impl.imageio; + +import java.io.IOException; +import java.util.Iterator; + +import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.stream.ImageInputStream; +import javax.xml.transform.Source; + +import org.apache.xmlgraphics.image.loader.ImageContext; +import org.apache.xmlgraphics.image.loader.ImageException; +import org.apache.xmlgraphics.image.loader.ImageInfo; +import org.apache.xmlgraphics.image.loader.ImageSize; +import org.apache.xmlgraphics.image.loader.impl.AbstractImagePreloader; +import org.apache.xmlgraphics.image.loader.util.ImageUtil; + +/** + * Image preloader for images supported by ImageIO. + *

    + * Note: The implementation relies on the presence of a working ImageIO implementation which + * provides accurate image metadata. This is particularly important for PNG image because the + * PNG loader relies on that. + */ +public class PreloaderImageIO extends AbstractImagePreloader { + + /** {@inheritDoc} + * @throws ImageException */ + public ImageInfo preloadImage(String uri, Source src, ImageContext context) + throws IOException, ImageException { + if (!ImageUtil.hasImageInputStream(src)) { + return null; + } + ImageInputStream in = ImageUtil.needImageInputStream(src); + Iterator iter = ImageIO.getImageReaders(in); + if (!iter.hasNext()) { + return null; + } + + IOException firstIOException = null; + IIOMetadata iiometa = null; + ImageSize size = null; + String mime = null; + while (iter.hasNext()) { + in.mark(); + + ImageReader reader = (ImageReader)iter.next(); + try { + reader.setInput(ImageUtil.ignoreFlushing(in), true, false); + final int imageIndex = 0; + iiometa = reader.getImageMetadata(imageIndex); + size = new ImageSize(); + size.setSizeInPixels(reader.getWidth(imageIndex), reader.getHeight(imageIndex)); + mime = reader.getOriginatingProvider().getMIMETypes()[0]; + break; + } catch (IOException ioe) { + //remember the first exception, ignore all others and continue + if (firstIOException == null) { + firstIOException = ioe; + } + } finally { + reader.dispose(); + in.reset(); + } + } + + if (iiometa == null) { + if (firstIOException == null) { + throw new ImageException("Could not extract image metadata"); + } else { + throw new ImageException("I/O error while extracting image metadata" + + (firstIOException.getMessage() != null + ? ": " + firstIOException.getMessage() + : ""), + firstIOException); + } + } + + //Resolution (first a default, then try to read the metadata) + size.setResolution(context.getSourceResolution()); + ImageIOUtil.extractResolution(iiometa, size); + if (size.getWidthPx() <= 0 || size.getHeightPx() <= 0) { + //Watch out for a special case: a TGA image was erroneously identified + //as a WBMP image by a Sun ImageIO codec. + return null; + } + if (size.getWidthMpt() == 0) { + size.calcSizeFromPixels(); + } + + ImageInfo info = new ImageInfo(uri, mime); + info.getCustomObjects().put(ImageIOUtil.IMAGEIO_METADATA, iiometa); + info.setSize(size); + + return info; + } + + /** {@inheritDoc} */ + public int getPriority() { + //Lower priority than default to give the specialized preloaders a chance. + return 2 * DEFAULT_PRIORITY; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/impl/imageio/package.html b/src/main/java/org/apache/xmlgraphics/image/loader/impl/imageio/package.html new file mode 100644 index 0000000..a153642 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/impl/imageio/package.html @@ -0,0 +1,25 @@ + + + +org.apache.fop.image2.impl.imageio Package + +

    + Contains an implementation of an image loader which uses ImageIO. +

    + + \ No newline at end of file diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/impl/package.html b/src/main/java/org/apache/xmlgraphics/image/loader/impl/package.html new file mode 100644 index 0000000..e8e8911 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/impl/package.html @@ -0,0 +1,25 @@ + + + +org.apache.fop.image2.impl Package + +

    + Contains implementations of image loaders and converters. +

    + + \ No newline at end of file diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/package.html b/src/main/java/org/apache/xmlgraphics/image/loader/package.html new file mode 100644 index 0000000..65bc8bd --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/package.html @@ -0,0 +1,26 @@ + + + +org.apache.fop.image2 Package + +

    + Contains image loading and conversion infrastructure for various image + sources and an image cache. +

    + + \ No newline at end of file diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/pipeline/ImageConversionEdge.java b/src/main/java/org/apache/xmlgraphics/image/loader/pipeline/ImageConversionEdge.java new file mode 100644 index 0000000..f20bde7 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/pipeline/ImageConversionEdge.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageConversionEdge.java 924666 2010-03-18 08:26:30Z jeremias $ */ + +package org.apache.xmlgraphics.image.loader.pipeline; + +import org.apache.xmlgraphics.image.loader.spi.ImageConverter; +import org.apache.xmlgraphics.image.loader.util.Penalty; +import org.apache.xmlgraphics.util.dijkstra.Edge; +import org.apache.xmlgraphics.util.dijkstra.Vertex; + +/** + * Represents an image conversion. The class basically wraps an ImageConverter so it can be + * used with Dijkstra's shortest route algorithm to build image conversion pipelines. + */ +class ImageConversionEdge implements Edge { + + private ImageRepresentation source; + private ImageRepresentation target; + private ImageConverter converter; + private int penalty; + + /** + * Main constructor. + * @param converter the image converter + * @param penalty the penalty for this edge + */ + public ImageConversionEdge(ImageConverter converter, Penalty penalty) { + this.converter = converter; + this.source = new ImageRepresentation(converter.getSourceFlavor()); + this.target = new ImageRepresentation(converter.getTargetFlavor()); + this.penalty = Math.max(0, penalty.getValue()); + } + + /** + * Returns the wrapped ImageConverter. + * @return the ImageConverter + */ + public ImageConverter getImageConverter() { + return this.converter; + } + + /** {@inheritDoc} */ + public int getPenalty() { + return this.penalty; + } + + /** {@inheritDoc} */ + public Vertex getStart() { + return this.source; + } + + /** {@inheritDoc} */ + public Vertex getEnd() { + return this.target; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/pipeline/ImageProviderPipeline.java b/src/main/java/org/apache/xmlgraphics/image/loader/pipeline/ImageProviderPipeline.java new file mode 100644 index 0000000..152172b --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/pipeline/ImageProviderPipeline.java @@ -0,0 +1,319 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageProviderPipeline.java 1804124 2017-08-04 14:13:54Z ssteiner $ */ + +package org.apache.xmlgraphics.image.loader.pipeline; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.apache.xmlgraphics.image.loader.Image; +import org.apache.xmlgraphics.image.loader.ImageException; +import org.apache.xmlgraphics.image.loader.ImageFlavor; +import org.apache.xmlgraphics.image.loader.ImageInfo; +import org.apache.xmlgraphics.image.loader.ImageSessionContext; +import org.apache.xmlgraphics.image.loader.cache.ImageCache; +import org.apache.xmlgraphics.image.loader.impl.ImageRawStream; +import org.apache.xmlgraphics.image.loader.spi.ImageConverter; +import org.apache.xmlgraphics.image.loader.spi.ImageImplRegistry; +import org.apache.xmlgraphics.image.loader.spi.ImageLoader; +import org.apache.xmlgraphics.image.loader.util.Penalty; + +/** + * Represents a pipeline of ImageConverters with an ImageLoader at the beginning of the + * pipeline. + */ +public class ImageProviderPipeline { + + /** logger */ + protected static final Log log = LogFactory.getLog(ImageProviderPipeline.class); + + private ImageCache cache; + private ImageLoader loader; + private List converters = new java.util.ArrayList(); + + /** + * Main constructor. + * @param cache the image cache (may be null if no caching is desired) + * @param loader the image loader to drive the pipeline with + */ + public ImageProviderPipeline(ImageCache cache, ImageLoader loader) { + this.cache = cache; + setImageLoader(loader); + } + + /** + * Constructor for operation without caching. + * @param loader the image loader to drive the pipeline with + */ + public ImageProviderPipeline(ImageLoader loader) { + this(null, loader); + } + + /** + * Default constructor without caching and without an ImageLoader (or the ImageLoader may + * be set later). + */ + public ImageProviderPipeline() { + this(null, null); + } + + /** + * Executes the image converter pipeline. First, the image indicated by the ImageInfo instance + * is loaded through an ImageLoader and then optionally converted by a series of + * ImageConverters. At the end of the pipeline, the fully loaded and converted image is + * returned. + * @param info the image info object indicating the image to load + * @param hints a Map of image conversion hints + * @param context the session context + * @return the requested image + * @throws ImageException if an error occurs while loader or converting the image + * @throws IOException if an I/O error occurs + */ + public Image execute(ImageInfo info, Map hints, ImageSessionContext context) + throws ImageException, IOException { + return execute(info, null, hints, context); + } + + /** + * Executes the image converter pipeline. First, the image indicated by the ImageInfo instance + * is loaded through an ImageLoader and then optionally converted by a series of + * ImageConverters. At the end of the pipeline, the fully loaded and converted image is + * returned. + * @param info the image info object indicating the image to load + * @param originalImage the original image to start the pipeline off or null if an ImageLoader + * is used + * @param hints a Map of image conversion hints + * @param context the session context + * @return the requested image + * @throws ImageException if an error occurs while loader or converting the image + * @throws IOException if an I/O error occurs + */ + public Image execute(ImageInfo info, Image originalImage, + Map hints, ImageSessionContext context) throws ImageException, IOException { + if (hints == null) { + hints = Collections.EMPTY_MAP; + } + long start = System.currentTimeMillis(); + Image img = null; + + //Remember the last image in the pipeline that is cacheable and cache that. + Image lastCacheableImage = null; + + int converterCount = converters.size(); + int startingPoint = 0; + if (cache != null) { + for (int i = converterCount - 1; i >= 0; i--) { + ImageConverter converter = getConverter(i); + ImageFlavor flavor = converter.getTargetFlavor(); + img = cache.getImage(info, flavor); + if (img != null) { + startingPoint = i + 1; + break; + } + } + + if (img == null && loader != null) { + //try target flavor of loader from cache + ImageFlavor flavor = loader.getTargetFlavor(); + img = cache.getImage(info, flavor); + } + } + if (img == null && originalImage != null) { + img = originalImage; + } + + boolean entirelyInCache = true; + long duration; + if (img == null && loader != null) { + //Load image + img = loader.loadImage(info, hints, context); + if (log.isTraceEnabled()) { + duration = System.currentTimeMillis() - start; + log.trace("Image loading using " + loader + " took " + duration + " ms."); + } + + //Caching + entirelyInCache = false; + if (img.isCacheable()) { + lastCacheableImage = img; + } + } + if (img == null) { + throw new ImageException( + "Pipeline fails. No ImageLoader and no original Image available."); + } + + if (converterCount > 0) { + for (int i = startingPoint; i < converterCount; i++) { + ImageConverter converter = getConverter(i); + start = System.currentTimeMillis(); + img = converter.convert(img, hints); + if (log.isTraceEnabled()) { + duration = System.currentTimeMillis() - start; + log.trace("Image conversion using " + converter + " took " + duration + " ms."); + } + + //Caching + entirelyInCache = false; + if (img.isCacheable()) { + lastCacheableImage = img; + } + } + } + + //Note: Currently we just cache the end result of the pipeline, not all intermediate + //results as it is expected that the cache hit ration would be rather small. + if (cache != null && !entirelyInCache) { + if (lastCacheableImage == null) { + //Try to make the Image cacheable + lastCacheableImage = forceCaching(img); + } + if (lastCacheableImage != null) { + if (log.isTraceEnabled()) { + log.trace("Caching image: " + lastCacheableImage); + } + cache.putImage(lastCacheableImage); + } + } + return img; + } + + private ImageConverter getConverter(int index) { + return (ImageConverter)converters.get(index); + } + + /** + * In some cases the provided Image is not cacheable, nor is any of the intermediate Image + * instances (for example, when loading a raw JPEG file). If the image is loaded over a + * potentially slow network, it is preferrable to download the whole file and cache it + * in memory or in a temporary file. It's not always possible to convert an Image into a + * cacheable variant. + * @param img the Image to investigate + * @return the converted, cacheable Image or null if the Image cannot be converted + * @throws IOException if an I/O error occurs + */ + protected Image forceCaching(Image img) throws IOException { + if (img instanceof ImageRawStream) { + ImageRawStream raw = (ImageRawStream)img; + if (log.isDebugEnabled()) { + log.debug("Image is made cacheable: " + img.getInfo()); + } + + //Read the whole stream and hold it in memory so the image can be cached + ByteArrayOutputStream baout = new ByteArrayOutputStream(); + InputStream in = raw.createInputStream(); + try { + IOUtils.copy(in, baout); + } finally { + IOUtils.closeQuietly(in); + } + final byte[] data = baout.toByteArray(); + raw.setInputStreamFactory(new ImageRawStream.ByteArrayStreamFactory(data)); + return raw; + } + return null; + } + + /** + * Sets the ImageLoader that is used at the beginning of the pipeline if the image is not + * loaded, yet. + * @param imageLoader the image loader implementation + */ + public void setImageLoader(ImageLoader imageLoader) { + this.loader = imageLoader; + } + + /** + * Adds an additional ImageConverter to the end of the pipeline. + * @param converter the ImageConverter instance + */ + public void addConverter(ImageConverter converter) { + //TODO check for compatibility + this.converters.add(converter); + } + + /** {@inheritDoc} */ + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append("Loader: ").append(loader); + if (converters.size() > 0) { + sb.append(" Converters: "); + sb.append(converters); + } + return sb.toString(); + } + + /** + * Returns the overall conversion penalty for the pipeline. This can be used to choose among + * different possible pipelines. + * @return the overall penalty (a non-negative integer) + */ + public int getConversionPenalty() { + return getConversionPenalty(null).getValue(); + } + + /** + * Returns the overall conversion penalty for the pipeline. This can be used to choose among + * different possible pipelines. + * @param registry the image implementation registry + * @return the overall penalty (a non-negative integer) + */ + public Penalty getConversionPenalty(ImageImplRegistry registry) { + Penalty penalty = Penalty.ZERO_PENALTY; + if (loader != null) { + penalty = penalty.add(loader.getUsagePenalty()); + if (registry != null) { + penalty = penalty.add( + registry.getAdditionalPenalty(loader.getClass().getName())); + } + } + for (Object converter1 : converters) { + ImageConverter converter = (ImageConverter) converter1; + penalty = penalty.add(converter.getConversionPenalty()); + if (registry != null) { + penalty = penalty.add( + registry.getAdditionalPenalty(converter.getClass().getName())); + } + } + return penalty; + } + + /** + * Returns the target flavor generated by this pipeline. + * @return the target flavor + */ + public ImageFlavor getTargetFlavor() { + if (converters.size() > 0) { + return getConverter(converters.size() - 1).getTargetFlavor(); + } else if (this.loader != null) { + return this.loader.getTargetFlavor(); + } else { + return null; + } + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/pipeline/ImageRepresentation.java b/src/main/java/org/apache/xmlgraphics/image/loader/pipeline/ImageRepresentation.java new file mode 100644 index 0000000..b4202e3 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/pipeline/ImageRepresentation.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageRepresentation.java 1681108 2015-05-22 13:26:12Z ssteiner $ */ + +package org.apache.xmlgraphics.image.loader.pipeline; + +import org.apache.xmlgraphics.image.loader.ImageFlavor; +import org.apache.xmlgraphics.util.dijkstra.Vertex; + +/** + * This class represents a combination of MIME type and an image flavor. + * It is used in conjunction with Dijkstra's algorithm to find and construct a + * conversion pipeline for images. + */ +public class ImageRepresentation implements Vertex { + + private ImageFlavor flavor; + + /** + * Main constructor + * @param flavor the image flavor + */ + public ImageRepresentation(ImageFlavor flavor) { + if (flavor == null) { + throw new NullPointerException("flavor must not be null"); + } + this.flavor = flavor; + } + + /** + * Returns the image flavor. + * @return the image flavor + */ + public ImageFlavor getFlavor() { + return flavor; + } + + /** {@inheritDoc} */ + public boolean equals(Object obj) { + assert obj != null; + return toString().equals(obj.toString()); + } + + /** {@inheritDoc} */ + public int hashCode() { + return getFlavor().hashCode(); + } + + /** {@inheritDoc} */ + public int compareTo(Object obj) { + return toString().compareTo(((ImageRepresentation)obj).toString()); + } + + /** {@inheritDoc} */ + public String toString() { + return getFlavor().toString(); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/pipeline/PipelineFactory.java b/src/main/java/org/apache/xmlgraphics/image/loader/pipeline/PipelineFactory.java new file mode 100644 index 0000000..33a6d8a --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/pipeline/PipelineFactory.java @@ -0,0 +1,290 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PipelineFactory.java 1804124 2017-08-04 14:13:54Z ssteiner $ */ + +package org.apache.xmlgraphics.image.loader.pipeline; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.LinkedList; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.apache.xmlgraphics.image.loader.Image; +import org.apache.xmlgraphics.image.loader.ImageFlavor; +import org.apache.xmlgraphics.image.loader.ImageInfo; +import org.apache.xmlgraphics.image.loader.ImageManager; +import org.apache.xmlgraphics.image.loader.impl.CompositeImageLoader; +import org.apache.xmlgraphics.image.loader.spi.ImageConverter; +import org.apache.xmlgraphics.image.loader.spi.ImageImplRegistry; +import org.apache.xmlgraphics.image.loader.spi.ImageLoader; +import org.apache.xmlgraphics.image.loader.spi.ImageLoaderFactory; +import org.apache.xmlgraphics.image.loader.util.Penalty; +import org.apache.xmlgraphics.util.dijkstra.DefaultEdgeDirectory; +import org.apache.xmlgraphics.util.dijkstra.DijkstraAlgorithm; +import org.apache.xmlgraphics.util.dijkstra.Vertex; + +/** + * Factory class for image processing pipelines. + */ +public class PipelineFactory { + + /** logger */ + protected static final Log log = LogFactory.getLog(PipelineFactory.class); + + private ImageManager manager; + + private int converterEdgeDirectoryVersion = -1; + + /** Holds the EdgeDirectory for all image conversions */ + private DefaultEdgeDirectory converterEdgeDirectory; + + /** + * Main constructor. + * @param manager the ImageManager instance + */ + public PipelineFactory(ImageManager manager) { + this.manager = manager; + } + + private DefaultEdgeDirectory getEdgeDirectory() { + ImageImplRegistry registry = manager.getRegistry(); + if (registry.getImageConverterModifications() != converterEdgeDirectoryVersion) { + Collection converters = registry.getImageConverters(); + + //Rebuild edge directory + DefaultEdgeDirectory dir = new DefaultEdgeDirectory(); + for (Object converter1 : converters) { + ImageConverter converter = (ImageConverter) converter1; + Penalty penalty = Penalty.toPenalty(converter.getConversionPenalty()); + penalty = penalty.add( + registry.getAdditionalPenalty(converter.getClass().getName())); + dir.addEdge(new ImageConversionEdge(converter, penalty)); + } + + converterEdgeDirectoryVersion = registry.getImageConverterModifications(); + this.converterEdgeDirectory = dir; //Replace (thread-safe) + } + return this.converterEdgeDirectory; + } + + /** + * Creates and returns an {@link ImageProviderPipeline} that allows to load an image of the + * given MIME type and present it in the requested image flavor. + * @param originalImage the original image that serves as the origin point of the conversion + * @param targetFlavor the requested image flavor + * @return an {@link ImageProviderPipeline} or null if no suitable pipeline could be assembled + */ + public ImageProviderPipeline newImageConverterPipeline( + Image originalImage, ImageFlavor targetFlavor) { + //Get snapshot to avoid concurrent modification problems (thread-safety) + DefaultEdgeDirectory dir = getEdgeDirectory(); + ImageRepresentation destination = new ImageRepresentation(targetFlavor); + ImageProviderPipeline pipeline = findPipeline(dir, originalImage.getFlavor(), destination); + return pipeline; + } + + /** + * Creates and returns an {@link ImageProviderPipeline} that allows to load an image of the + * given MIME type and present it in the requested image flavor. + * @param imageInfo the image info object of the original image + * @param targetFlavor the requested image flavor + * @return an {@link ImageProviderPipeline} or null if no suitable pipeline could be assembled + */ + public ImageProviderPipeline newImageConverterPipeline( + ImageInfo imageInfo, ImageFlavor targetFlavor) { + ImageProviderPipeline[] candidates = determineCandidatePipelines(imageInfo, targetFlavor); + + //Choose best pipeline + if (candidates.length > 0) { + Arrays.sort(candidates, new PipelineComparator()); + ImageProviderPipeline pipeline = (ImageProviderPipeline)candidates[0]; + if (pipeline != null && log.isDebugEnabled()) { + log.debug("Pipeline: " + pipeline + + " with penalty " + pipeline.getConversionPenalty()); + } + return pipeline; + } else { + return null; + } + } + + /** + * Determines all possible pipelines for the given image that can produce the requested + * target flavor. + * @param imageInfo the image information + * @param targetFlavor the target flavor + * @return the candidate pipelines + */ + public ImageProviderPipeline[] determineCandidatePipelines( + ImageInfo imageInfo, ImageFlavor targetFlavor) { + String originalMime = imageInfo.getMimeType(); + ImageImplRegistry registry = manager.getRegistry(); + List candidates = new java.util.ArrayList(); + + //Get snapshot to avoid concurrent modification problems (thread-safety) + DefaultEdgeDirectory dir = getEdgeDirectory(); + + ImageLoaderFactory[] loaderFactories = registry.getImageLoaderFactories( + imageInfo, targetFlavor); + if (loaderFactories != null) { + //Directly load image and return it + ImageLoader loader; + if (loaderFactories.length == 1) { + loader = loaderFactories[0].newImageLoader(targetFlavor); + } else { + int count = loaderFactories.length; + ImageLoader[] loaders = new ImageLoader[count]; + for (int i = 0; i < count; i++) { + loaders[i] = loaderFactories[i].newImageLoader(targetFlavor); + } + loader = new CompositeImageLoader(loaders); + } + ImageProviderPipeline pipeline = new ImageProviderPipeline(manager.getCache(), loader); + candidates.add(pipeline); + } else { + //Need to use ImageConverters + if (log.isTraceEnabled()) { + log.trace("No ImageLoaderFactory found that can load this format (" + + targetFlavor + ") directly. Trying ImageConverters instead..."); + } + + ImageRepresentation destination = new ImageRepresentation(targetFlavor); + //Get Loader for originalMIME + // --> List of resulting flavors, possibly multiple loaders + loaderFactories = registry.getImageLoaderFactories(originalMime); + if (loaderFactories != null) { + + //Find best pipeline -> best loader + for (ImageLoaderFactory loaderFactory : loaderFactories) { + ImageFlavor[] flavors = loaderFactory.getSupportedFlavors(originalMime); + for (ImageFlavor flavor : flavors) { + ImageProviderPipeline pipeline = findPipeline(dir, flavor, destination); + if (pipeline != null) { + ImageLoader loader = loaderFactory.newImageLoader(flavor); + pipeline.setImageLoader(loader); + candidates.add(pipeline); + } + } + } + } + } + return (ImageProviderPipeline[])candidates.toArray( + new ImageProviderPipeline[candidates.size()]); + } + + /** Compares two pipelines based on their conversion penalty. */ + private static class PipelineComparator implements Comparator, Serializable { + + private static final long serialVersionUID = 1161513617996198090L; + + public int compare(Object o1, Object o2) { + ImageProviderPipeline p1 = (ImageProviderPipeline)o1; + ImageProviderPipeline p2 = (ImageProviderPipeline)o2; + //Lowest penalty first + return p1.getConversionPenalty() - p2.getConversionPenalty(); + } + + } + + private ImageProviderPipeline findPipeline(DefaultEdgeDirectory dir, + ImageFlavor originFlavor, ImageRepresentation destination) { + DijkstraAlgorithm dijkstra = new DijkstraAlgorithm( + dir); + ImageRepresentation origin = new ImageRepresentation(originFlavor); + dijkstra.execute(origin, destination); + if (log.isTraceEnabled()) { + log.trace("Lowest penalty: " + dijkstra.getLowestPenalty(destination)); + } + + Vertex prev = destination; + Vertex pred = dijkstra.getPredecessor(destination); + if (pred == null) { + if (log.isTraceEnabled()) { + log.trace("No route found!"); + } + return null; + } else { + LinkedList stops = new LinkedList(); + while ((pred = dijkstra.getPredecessor(prev)) != null) { + ImageConversionEdge edge = (ImageConversionEdge) + dir.getBestEdge(pred, prev); + stops.addFirst(edge); + prev = pred; + } + ImageProviderPipeline pipeline = new ImageProviderPipeline(manager.getCache(), null); + for (Object stop : stops) { + ImageConversionEdge edge = (ImageConversionEdge) stop; + pipeline.addConverter(edge.getImageConverter()); + } + return pipeline; + } + } + + /** + * Finds and returns an array of {@link ImageProviderPipeline} instances which can handle + * the given MIME type and return one of the given {@link ImageFlavor}s. + * @param imageInfo the image info object + * @param flavors the possible target flavors + * @return an array of pipelines + */ + public ImageProviderPipeline[] determineCandidatePipelines(ImageInfo imageInfo, + ImageFlavor[] flavors) { + List candidates = new java.util.ArrayList(); + for (ImageFlavor flavor : flavors) { + //Find the best pipeline for each flavor + ImageProviderPipeline pipeline = newImageConverterPipeline(imageInfo, flavor); + if (pipeline == null) { + continue; //No suitable pipeline found for flavor + } + Penalty p = pipeline.getConversionPenalty(this.manager.getRegistry()); + if (!p.isInfinitePenalty()) { + candidates.add(pipeline); + } + } + return (ImageProviderPipeline[])candidates.toArray( + new ImageProviderPipeline[candidates.size()]); + } + + /** + * Finds and returns an array of {@link ImageProviderPipeline} instances which can handle + * the convert the given {@link Image} and return one of the given {@link ImageFlavor}s. + * @param sourceImage the image to be converted + * @param flavors the possible target flavors + * @return an array of pipelines + */ + public ImageProviderPipeline[] determineCandidatePipelines(Image sourceImage, + ImageFlavor[] flavors) { + List candidates = new java.util.ArrayList(); + for (ImageFlavor flavor : flavors) { + //Find the best pipeline for each flavor + ImageProviderPipeline pipeline = newImageConverterPipeline(sourceImage, flavor); + if (pipeline != null) { + candidates.add(pipeline); + } + } + return (ImageProviderPipeline[])candidates.toArray( + new ImageProviderPipeline[candidates.size()]); + } + + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/pipeline/package.html b/src/main/java/org/apache/xmlgraphics/image/loader/pipeline/package.html new file mode 100644 index 0000000..a64dfab --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/pipeline/package.html @@ -0,0 +1,25 @@ + + + +org.apache.fop.image2.pipeline Package + +

    + Provides an image loading and processing pipeline. +

    + + \ No newline at end of file diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/spi/ImageConverter.java b/src/main/java/org/apache/xmlgraphics/image/loader/spi/ImageConverter.java new file mode 100644 index 0000000..fcdf006 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/spi/ImageConverter.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageConverter.java 1400596 2012-10-21 08:49:02Z gadams $ */ + +package org.apache.xmlgraphics.image.loader.spi; + +import java.io.IOException; +import java.util.Map; + +import org.apache.xmlgraphics.image.loader.Image; +import org.apache.xmlgraphics.image.loader.ImageException; +import org.apache.xmlgraphics.image.loader.ImageFlavor; + +/** + * Defines an image converter that can convert one image representation into another. + */ +public interface ImageConverter { + + /** Used if the conversion penalty is negligible (for example a simple cast). */ + int NO_CONVERSION_PENALTY = 0; + /** Used if the conversion penalty is minimal */ + int MINIMAL_CONVERSION_PENALTY = 1; + /** Default/Medium conversion penalty (if there's some effort to convert the image format) */ + int MEDIUM_CONVERSION_PENALTY = 10; + + /** + * Converts an image into a different representation. + *

    Consumers can get the effective MIME type (if any) from the + * associated {@link ImageFlavor}. + * @param src the source image + * @param hints the conversion hints + * @return the converted image + * @throws ImageException if an error occurs while converting the image + * @throws IOException if an I/O error occurs while converting the image + */ + Image convert(Image src, Map hints) throws ImageException, IOException; + + /** + * Returns the flavor that this converter converts images into. + * @return the target flavor + */ + ImageFlavor getTargetFlavor(); + + /** + * Returns the flavor that this converter expects. + * @return the source flavor + */ + ImageFlavor getSourceFlavor(); + + /** + * Returns the conversion penalty for the conversion that this implementation supports. + * @return the conversion penalty (must be a non-negative integer) + */ + int getConversionPenalty(); + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/spi/ImageImplRegistry.java b/src/main/java/org/apache/xmlgraphics/image/loader/spi/ImageImplRegistry.java new file mode 100644 index 0000000..8f74ea7 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/spi/ImageImplRegistry.java @@ -0,0 +1,427 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageImplRegistry.java 1804124 2017-08-04 14:13:54Z ssteiner $ */ + +package org.apache.xmlgraphics.image.loader.spi; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.apache.xmlgraphics.image.loader.ImageFlavor; +import org.apache.xmlgraphics.image.loader.ImageInfo; +import org.apache.xmlgraphics.image.loader.util.Penalty; +import org.apache.xmlgraphics.util.Service; + +/** + * This class is the registry for all implementations of the various service provider interfaces + * for the image package. + */ +public class ImageImplRegistry { + + /** logger */ + protected static final Log log = LogFactory.getLog(ImageImplRegistry.class); + + /** Infinite penalty value which shall force any implementation to become ineligible. */ + public static final int INFINITE_PENALTY = Integer.MAX_VALUE; + + /** Holds the list of preloaders */ + private List preloaders = new java.util.ArrayList(); + //Content: List + private int lastPreloaderIdentifier; + private int lastPreloaderSort; + + /** Holds the list of ImageLoaderFactories */ + private Map loaders = new java.util.HashMap(); + //Content: Map> + + /** Holds the list of ImageConverters */ + private List converters = new java.util.ArrayList(); + //Content: List + + private int converterModifications; + + /** A Map (key: implementation classes) with additional penalties to fine-tune the registry. */ + private Map additionalPenalties = new java.util.HashMap(); // + //Note: String as key chosen to avoid possible class-unloading leaks + + /** Singleton instance */ + private static ImageImplRegistry defaultInstance = new ImageImplRegistry(); + + /** + * Main constructor. This constructor allows to disable plug-in discovery for testing purposes. + * @param discover true if implementation classes shall automatically be discovered. + */ + public ImageImplRegistry(boolean discover) { + if (discover) { + discoverClasspathImplementations(); + } + } + + /** + * Main constructor. + */ + public ImageImplRegistry() { + this(true); + } + + /** + * Returns the default instance of the Image implementation registry. + * @return the default instance + */ + public static ImageImplRegistry getDefaultInstance() { + return defaultInstance; + } + + /** + * Discovers all implementations in the application's classpath. + */ + public void discoverClasspathImplementations() { + //Dynamic registration of ImagePreloaders + Iterator iter = Service.providers(ImagePreloader.class); + while (iter.hasNext()) { + registerPreloader((ImagePreloader)iter.next()); + } + + //Dynamic registration of ImageLoaderFactories + iter = Service.providers(ImageLoaderFactory.class); + while (iter.hasNext()) { + registerLoaderFactory((ImageLoaderFactory)iter.next()); + } + + //Dynamic registration of ImageConverters + iter = Service.providers(ImageConverter.class); + while (iter.hasNext()) { + registerConverter((ImageConverter)iter.next()); + } + } + + /** + * Registers a new ImagePreloader. + * @param preloader An ImagePreloader instance + */ + public void registerPreloader(ImagePreloader preloader) { + if (log.isDebugEnabled()) { + log.debug("Registered " + preloader.getClass().getName() + + " with priority " + preloader.getPriority()); + } + preloaders.add(newPreloaderHolder(preloader)); + } + + private synchronized PreloaderHolder newPreloaderHolder(ImagePreloader preloader) { + PreloaderHolder holder = new PreloaderHolder(); + holder.preloader = preloader; + holder.identifier = ++lastPreloaderIdentifier; + return holder; + } + + /** Holder class for registered {@link ImagePreloader} instances. */ + private static class PreloaderHolder { + private ImagePreloader preloader; + private int identifier; + + public String toString() { + return preloader + " " + identifier; + } + } + + private synchronized void sortPreloaders() { + if (this.lastPreloaderIdentifier != this.lastPreloaderSort) { + Collections.sort(this.preloaders, new Comparator() { + + public int compare(Object o1, Object o2) { + PreloaderHolder h1 = (PreloaderHolder)o1; + long p1 = h1.preloader.getPriority(); + p1 += getAdditionalPenalty(h1.preloader.getClass().getName()).getValue(); + + PreloaderHolder h2 = (PreloaderHolder)o2; + int p2 = h2.preloader.getPriority(); + p2 += getAdditionalPenalty(h2.preloader.getClass().getName()).getValue(); + + int diff = Penalty.truncate(p1 - p2); + if (diff != 0) { + return diff; + } else { + diff = h1.identifier - h2.identifier; + return diff; + } + } + + }); + this.lastPreloaderSort = lastPreloaderIdentifier; + } + } + + /** + * Registers a new ImageLoaderFactory. + * @param loaderFactory An ImageLoaderFactory instance + */ + public void registerLoaderFactory(ImageLoaderFactory loaderFactory) { + if (!loaderFactory.isAvailable()) { + if (log.isDebugEnabled()) { + log.debug("ImageLoaderFactory reports not available: " + + loaderFactory.getClass().getName()); + } + return; + } + String[] mimes = loaderFactory.getSupportedMIMETypes(); + for (String mime : mimes) { + synchronized (loaders) { + Map flavorMap = (Map) loaders.get(mime); + if (flavorMap == null) { + flavorMap = new HashMap(); + loaders.put(mime, flavorMap); + } + + ImageFlavor[] flavors = loaderFactory.getSupportedFlavors(mime); + for (ImageFlavor flavor : flavors) { + List factoryList = (List) flavorMap.get(flavor); + if (factoryList == null) { + factoryList = new ArrayList(); + flavorMap.put(flavor, factoryList); + } + factoryList.add(loaderFactory); + + if (log.isDebugEnabled()) { + log.debug("Registered " + loaderFactory.getClass().getName() + + ": MIME = " + mime + ", Flavor = " + flavor); + } + } + } + } + } + + /** + * Returns the Collection of registered ImageConverter instances. + * @return a Collection<ImageConverter> + */ + public Collection getImageConverters() { + return Collections.unmodifiableList(this.converters); + } + + /** + * Returns the number of modifications to the collection of registered ImageConverter instances. + * This is used to detect changes in the registry concerning ImageConverters. + * @return the number of modifications + */ + public int getImageConverterModifications() { + return this.converterModifications; + } + + /** + * Registers a new ImageConverter. + * @param converter An ImageConverter instance + */ + public void registerConverter(ImageConverter converter) { + converters.add(converter); + converterModifications++; + if (log.isDebugEnabled()) { + log.debug("Registered: " + converter.getClass().getName()); + } + } + + /** + * Returns an iterator over all registered ImagePreloader instances. + * @return an iterator over ImagePreloader instances. + */ + public Iterator getPreloaderIterator() { + sortPreloaders(); + final Iterator iter = this.preloaders.iterator(); + //Unpack the holders + MyIterator i = new MyIterator(); + i.iter = iter; + return i; + } + + static class MyIterator implements Iterator { + Iterator iter; + public boolean hasNext() { + return iter.hasNext(); + } + + public Object next() { + Object obj = iter.next(); + if (obj != null) { + return ((PreloaderHolder)obj).preloader; + } else { + return null; + } + } + + public void remove() { + iter.remove(); + } + + } + + /** + * Returns the best ImageLoaderFactory supporting the {@link ImageInfo} and image flavor. + * If there are multiple ImageLoaderFactories the one with the least usage penalty is selected. + * @param imageInfo the image info object + * @param flavor the image flavor. + * @return an ImageLoaderFactory instance or null, if no suitable implementation was found + */ + public ImageLoaderFactory getImageLoaderFactory(ImageInfo imageInfo, ImageFlavor flavor) { + String mime = imageInfo.getMimeType(); + Map flavorMap = (Map)loaders.get(mime); + if (flavorMap != null) { + List factoryList = (List)flavorMap.get(flavor); + if (factoryList != null && factoryList.size() > 0) { + Iterator iter = factoryList.iterator(); + int bestPenalty = Integer.MAX_VALUE; + ImageLoaderFactory bestFactory = null; + while (iter.hasNext()) { + ImageLoaderFactory factory = (ImageLoaderFactory)iter.next(); + if (!factory.isSupported(imageInfo)) { + continue; + } + ImageLoader loader = factory.newImageLoader(flavor); + int penalty = loader.getUsagePenalty(); + if (penalty < bestPenalty) { + bestPenalty = penalty; + bestFactory = factory; + } + } + return bestFactory; + } + } + return null; + } + + /** + * Returns an array of {@link ImageLoaderFactory} instances that support the MIME type + * indicated by an {@link ImageInfo} object and can generate the given image flavor. + * @param imageInfo the image info object + * @param flavor the target image flavor + * @return the array of image loader factories + */ + public ImageLoaderFactory[] getImageLoaderFactories(ImageInfo imageInfo, ImageFlavor flavor) { + String mime = imageInfo.getMimeType(); + Collection matches = new java.util.TreeSet(new ImageLoaderFactoryComparator(flavor)); + Map flavorMap = (Map) loaders.get(mime); + if (flavorMap != null) { + for (Object i : flavorMap.entrySet()) { + Map.Entry e = (Map.Entry) i; + ImageFlavor checkFlavor = (ImageFlavor) e.getKey(); + if (checkFlavor.isCompatible(flavor)) { + List factoryList = (List)e.getValue(); + if (factoryList != null && factoryList.size() > 0) { + for (Object aFactoryList : factoryList) { + ImageLoaderFactory factory = (ImageLoaderFactory) aFactoryList; + if (factory.isSupported(imageInfo)) { + matches.add(factory); + } + } + } + } + } + } + if (matches.size() == 0) { + return null; + } else { + return (ImageLoaderFactory[])matches.toArray(new ImageLoaderFactory[matches.size()]); + } + } + + /** Comparator for {@link ImageLoaderFactory} classes. */ + private class ImageLoaderFactoryComparator implements Comparator { + + private ImageFlavor targetFlavor; + + public ImageLoaderFactoryComparator(ImageFlavor targetFlavor) { + this.targetFlavor = targetFlavor; + } + + public int compare(Object o1, Object o2) { + ImageLoaderFactory f1 = (ImageLoaderFactory)o1; + ImageLoader l1 = f1.newImageLoader(targetFlavor); + long p1 = l1.getUsagePenalty(); + p1 += getAdditionalPenalty(l1.getClass().getName()).getValue(); + + ImageLoaderFactory f2 = (ImageLoaderFactory)o2; + ImageLoader l2 = f2.newImageLoader(targetFlavor); +// long p2 = l2.getUsagePenalty(); + long p2 = getAdditionalPenalty(l2.getClass().getName()).getValue(); + + //Lowest penalty first + return Penalty.truncate(p1 - p2); + } + + } + + + /** + * Returns an array of ImageLoaderFactory instances which support the given MIME type. The + * instances are returned in no particular order. + * @param mime the MIME type to find ImageLoaderFactories for + * @return the array of ImageLoaderFactory instances + */ + public ImageLoaderFactory[] getImageLoaderFactories(String mime) { + Map flavorMap = (Map)loaders.get(mime); + if (flavorMap != null) { + Set factories = new java.util.HashSet(); + for (Object o : flavorMap.values()) { + List factoryList = (List) o; + factories.addAll(factoryList); + } + int factoryCount = factories.size(); + if (factoryCount > 0) { + return (ImageLoaderFactory[])factories.toArray( + new ImageLoaderFactory[factoryCount]); + } + } + return new ImageLoaderFactory[0]; + } + + /** + * Sets an additional penalty for a particular implementation class for any of the interface + * administered by this registry class. No checking is performed to verify if the className + * parameter is valid. + * @param className the fully qualified class name of the implementation class + * @param penalty the additional penalty or null to clear any existing value + */ + public void setAdditionalPenalty(String className, Penalty penalty) { + if (penalty != null) { + this.additionalPenalties.put(className, penalty); + } else { + this.additionalPenalties.remove(className); + } + this.lastPreloaderSort = -1; //Force resort, just in case this was a preloader + } + + /** + * Returns the additional penalty value set for a particular implementation class. + * If no such value is set, 0 is returned. + * @param className the fully qualified class name of the implementation class + * @return the additional penalty value + */ + public Penalty getAdditionalPenalty(String className) { + Penalty p = (Penalty)this.additionalPenalties.get(className); + return (p != null ? p : Penalty.ZERO_PENALTY); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/spi/ImageLoader.java b/src/main/java/org/apache/xmlgraphics/image/loader/spi/ImageLoader.java new file mode 100644 index 0000000..36a0d1f --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/spi/ImageLoader.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageLoader.java 924666 2010-03-18 08:26:30Z jeremias $ */ + +package org.apache.xmlgraphics.image.loader.spi; + +import java.io.IOException; +import java.util.Map; + +import org.apache.xmlgraphics.image.loader.Image; +import org.apache.xmlgraphics.image.loader.ImageException; +import org.apache.xmlgraphics.image.loader.ImageFlavor; +import org.apache.xmlgraphics.image.loader.ImageInfo; +import org.apache.xmlgraphics.image.loader.ImageSessionContext; + +/** + * This interface is implemented by classes which load images from a source. Normally, such a + * source will be an InputStream but can also be something else. + */ +public interface ImageLoader { + + /** Used if the loading penalty is negligible (image doesn't need to be decoded). */ + int NO_LOADING_PENALTY = 0; + /** Default/Medium conversion penalty (if there's some effort to load the image format) */ + int MEDIUM_LOADING_PENALTY = 10; + + /** + * Loads and returns an image. + * @param info the image info object indicating the image + * @param hints a Map of hints that can be used by implementations to customize the loading + * process (may be null). + * @param session the session context + * @return the fully loaded image + * @throws ImageException if an error occurs while loading the image + * @throws IOException if an I/O error occurs while loading the image + */ + Image loadImage(ImageInfo info, Map hints, ImageSessionContext session) + throws ImageException, IOException; + + /** + * Loads and returns an image. + * @param info the image info object indicating the image + * @param session the session context + * @return the fully loaded image + * @throws ImageException if an error occurs while loading the image + * @throws IOException if an I/O error occurs while loading the image + */ + Image loadImage(ImageInfo info, ImageSessionContext session) + throws ImageException, IOException; + + /** + * Returns the image flavor that is returned by this ImageLoader implementation. + * @return the target image flavor + */ + ImageFlavor getTargetFlavor(); + + /** + * Returns the penalty assigned to using this image loader. The value is used to select the + * best processing chain for images. + * @return the usage penalty (must be a non-negative integer) + */ + int getUsagePenalty(); + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/spi/ImageLoaderFactory.java b/src/main/java/org/apache/xmlgraphics/image/loader/spi/ImageLoaderFactory.java new file mode 100644 index 0000000..da7340c --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/spi/ImageLoaderFactory.java @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageLoaderFactory.java 924666 2010-03-18 08:26:30Z jeremias $ */ + +package org.apache.xmlgraphics.image.loader.spi; + +import org.apache.xmlgraphics.image.loader.ImageFlavor; +import org.apache.xmlgraphics.image.loader.ImageInfo; + +/** + * This interface is implemented to provide information about an ImageLoader and to create new + * instances. A separate factory allows implementation to dynamically detect if the underlying + * libraries are available in the classpath so the caller can skip this implementation if it's + * not functional. + */ +public interface ImageLoaderFactory { + + /** + * Returns an array of MIME types supported by this implementation. + * @return the MIME type array + */ + String[] getSupportedMIMETypes(); + + /** + * Returns an array of ImageFlavors that are supported by this implementation for a given + * MIME type. + * @param mime the MIME type + * @return the ImageFlavor array + */ + ImageFlavor[] getSupportedFlavors(String mime); + + /** + * Indicates whether the given image (represented by an {@link ImageInfo} object) is supported + * by the loader. By default, implementations return true assuming all images of the supported + * MIME types can be processed correctly. In some cases, however, an ImageLoader may only + * support a subset of a format because it offers an optimized way to embed the image in + * the target format (for example: CCITT compressed TIFF files in PDF and PostScript). For + * this to work, the preloader must register some information in the ImageInfo's custom + * objects so the factory can identify if an image may or may not be supported. + * @param imageInfo the image info object + * @return true if the image is supported by the loaders generated by this factory + */ + boolean isSupported(ImageInfo imageInfo); + + /** + * Creates and returns a new ImageLoader instance. + * @param targetFlavor the target image flavor to produce + * @return a new ImageLoader instance + */ + ImageLoader newImageLoader(ImageFlavor targetFlavor); + + /** + * Returns the usage penalty for a particular ImageLoader. This is used to select the best + * ImageLoader implementation for loading an image. + * @param mime the MIME type + * @param flavor the target image flavor + * @return the usage penalty (must be a non-negative integer) + * @deprecated Redundancy with {@link ImageLoader#getUsagePenalty()} + */ + int getUsagePenalty(String mime, ImageFlavor flavor); + + /** + * Indicates whether the underlying libraries needed by the implementation are available. + * @return true if the implementation is functional. + */ + boolean isAvailable(); + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/spi/ImagePreloader.java b/src/main/java/org/apache/xmlgraphics/image/loader/spi/ImagePreloader.java new file mode 100644 index 0000000..ee1a3ed --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/spi/ImagePreloader.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImagePreloader.java 750116 2009-03-04 19:23:59Z jeremias $ */ + +package org.apache.xmlgraphics.image.loader.spi; + +import java.io.IOException; + +import javax.xml.transform.Source; + +import org.apache.xmlgraphics.image.loader.ImageContext; +import org.apache.xmlgraphics.image.loader.ImageException; +import org.apache.xmlgraphics.image.loader.ImageInfo; + +/** + * This interface provides two functions: determining whether an image format is supported and if + * that's the case, some minimal information (mostly the image's intrinsic size) is extracted + * and returned. + */ +public interface ImagePreloader { + + /** Default priority for preloaders */ + int DEFAULT_PRIORITY = 1000; + + /** + * "Preloads" an image, i.e. indentifies whether the source image is supported by this + * implementation and determines the image's intrinsic size and possibly some additional + * information. The image is usually not fully loaded at this time to conserve memory. + * The method returns null if the image was not identified. An {@link ImageException} is only + * thrown if the image is identified but some error has happened while working on the file. + * @param originalURI the original (unresolved) URI of the image + * @param src a image source the image is loaded from + * @param context the context object that provides configuration information + * @return an image info object with the basic information about an image or null if the + * image is not supported by this implementation + * @throws ImageException if an error occurs while preloading the image + * @throws IOException if an I/O error occurs while preloading the image + */ + ImageInfo preloadImage(String originalURI, + Source src, ImageContext context) throws ImageException, IOException; + + /** + * Returns the priority of the preloader. The lower the value, the higher the preloader's + * priority. + * @return an integer (default is 1000) + */ + int getPriority(); +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/spi/package.html b/src/main/java/org/apache/xmlgraphics/image/loader/spi/package.html new file mode 100644 index 0000000..45d6af1 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/spi/package.html @@ -0,0 +1,25 @@ + + + +org.apache.fop.image2.spi Package + +

    + Defines service provider interfaces for the image infrastructure. +

    + + \ No newline at end of file diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/util/ImageInputStreamAdapter.java b/src/main/java/org/apache/xmlgraphics/image/loader/util/ImageInputStreamAdapter.java new file mode 100644 index 0000000..08bbbcf --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/util/ImageInputStreamAdapter.java @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageInputStreamAdapter.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.image.loader.util; + +import java.io.IOException; +import java.io.InputStream; + +import javax.imageio.stream.ImageInputStream; + +/** + * Decorates an ImageInputStream with an InputStream interface. The methods mark() + * and reset() are fully supported. The method available() will + * always return 0. + */ +public class ImageInputStreamAdapter extends InputStream { + + private ImageInputStream iin; + + private long lastMarkPosition; + + /** + * Creates a new ImageInputStreamAdapter. + * @param iin the underlying ImageInputStream + */ + public ImageInputStreamAdapter(ImageInputStream iin) { + assert iin != null : "InputStream is null"; + this.iin = iin; + } + + /** {@inheritDoc} */ + public int read(byte[] b, int off, int len) throws IOException { + return iin.read(b, off, len); + } + + /** {@inheritDoc} */ + public int read(byte[] b) throws IOException { + return iin.read(b); + } + + /** {@inheritDoc} */ + public int read() throws IOException { + return iin.read(); + } + + /** {@inheritDoc} */ + public long skip(long n) throws IOException { + return iin.skipBytes(n); + } + + /** {@inheritDoc} */ + public void close() throws IOException { + iin.close(); + iin = null; + } + + /** {@inheritDoc} */ + public synchronized void mark(int readlimit) { + //Parameter readlimit is ignored + try { + //Cannot use mark()/reset() since they are nestable, and InputStream's are not + this.lastMarkPosition = iin.getStreamPosition(); + } catch (IOException ioe) { + throw new RuntimeException( + "Unexpected IOException in ImageInputStream.getStreamPosition()", ioe); + } + } + + /** {@inheritDoc} */ + public boolean markSupported() { + return true; + } + + /** {@inheritDoc} */ + public synchronized void reset() throws IOException { + iin.seek(this.lastMarkPosition); + } + + /** {@inheritDoc} */ + public int available() throws IOException { + return 0; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/util/ImageUtil.java b/src/main/java/org/apache/xmlgraphics/image/loader/util/ImageUtil.java new file mode 100644 index 0000000..89a146f --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/util/ImageUtil.java @@ -0,0 +1,323 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageUtil.java 1681108 2015-05-22 13:26:12Z ssteiner $ */ + +package org.apache.xmlgraphics.image.loader.util; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Map; +import java.util.zip.GZIPInputStream; + +import javax.imageio.stream.ImageInputStream; +import javax.xml.transform.Source; + +import org.apache.xmlgraphics.image.loader.ImageProcessingHints; +import org.apache.xmlgraphics.image.loader.ImageSessionContext; +import org.apache.xmlgraphics.image.loader.ImageSource; +import org.apache.xmlgraphics.io.XmlSourceUtil; + +/** + * Helper and convenience methods for working with the image package. + */ +public final class ImageUtil { + + private ImageUtil() { + } + + /** + * Returns the InputStream of a Source object. + * @param src the Source object + * @return the InputStream (or null if there's not InputStream available) + * @deprecated Please use {@link XmlSourceUtil#getInputStream(Source)} instead. + */ + @Deprecated + public static InputStream getInputStream(Source src) { + return XmlSourceUtil.getInputStream(src); + } + + /** + * Returns the ImageInputStream of a Source object. + * @param src the Source object + * @return the ImageInputStream (or null if there's not ImageInputStream available) + */ + public static ImageInputStream getImageInputStream(Source src) { + if (src instanceof ImageSource) { + return ((ImageSource) src).getImageInputStream(); + } else { + return null; + } + } + + /** + * Returns the InputStream of a Source object. This method throws an IllegalArgumentException + * if there's no InputStream instance available from the Source object. + * @param src the Source object + * @return the InputStream + * @deprecated use {@link XmlSourceUtil#needInputStream(Source)} instead + */ + @Deprecated + public static InputStream needInputStream(Source src) { + return XmlSourceUtil.needInputStream(src); + } + + /** + * Returns the ImageInputStream of a Source object. This method throws an + * IllegalArgumentException if there's no ImageInputStream instance available from the + * Source object. + * @param src the Source object + * @return the ImageInputStream + */ + public static ImageInputStream needImageInputStream(Source src) { + if (src instanceof ImageSource) { + ImageSource isrc = (ImageSource) src; + if (isrc.getImageInputStream() == null) { + throw new IllegalArgumentException( + "ImageInputStream is null/cleared on ImageSource"); + } + return isrc.getImageInputStream(); + } else { + throw new IllegalArgumentException("Source must be an ImageSource"); + } + } + + /** + * Indicates whether the Source object has an InputStream instance. + * @param src the Source object + * @return true if an InputStream is available + */ + public static boolean hasInputStream(Source src) { + return XmlSourceUtil.hasInputStream(src) || hasImageInputStream(src); + } + + /** + * Indicates whether the Source object has a Reader instance. + * @param src the Source object + * @return true if an Reader is available + * @deprecated use {@link XmlSourceUtil#hasReader(Source)} instead + */ + @Deprecated + public static boolean hasReader(Source src) { + return XmlSourceUtil.hasReader(src); + } + + /** + * Indicates whether the Source object has an ImageInputStream instance. + * @param src the Source object + * @return true if an ImageInputStream is available + */ + public static boolean hasImageInputStream(Source src) { + return getImageInputStream(src) != null; + } + + /** + * Removes any references to InputStreams or Readers from the given Source to prohibit + * accidental/unwanted use by a component further downstream. + * @param src the Source object + * @deprecated use {@link XmlSourceUtil#removeStreams(Source)} instead + */ + @Deprecated + public static void removeStreams(Source src) { + XmlSourceUtil.removeStreams(src); + } + + /** + * Closes the InputStreams or ImageInputStreams of Source objects. Any exception occurring + * while closing the stream is ignored. + * @param src the Source object + * @deprecated use {@link XmlSourceUtil#closeQuietly(Source)} instead + */ + @Deprecated + public static void closeQuietly(Source src) { + XmlSourceUtil.closeQuietly(src); + } + + /** + * Decorates an ImageInputStream so the flush*() methods are ignored and have no effect. + * The decoration is implemented using a dynamic proxy. + * @param in the ImageInputStream + * @return the decorated ImageInputStream + */ + public static ImageInputStream ignoreFlushing(final ImageInputStream in) { + return (ImageInputStream) Proxy.newProxyInstance(in.getClass().getClassLoader(), + new Class[] {ImageInputStream.class}, + new InvocationHandler() { + public Object invoke(Object proxy, Method method, Object[] args) + throws Throwable { + String methodName = method.getName(); + //Ignore calls to flush*() + if (!methodName.startsWith("flush")) { + try { + return method.invoke(in, args); + } catch (InvocationTargetException ite) { + throw ite.getCause(); + } + } else { + return null; + } + } + }); + } + + /** + * GZIP header magic number bytes, like found in a gzipped + * files, which are encoded in Intel format (i..e. little indian). + */ + private static final byte[] GZIP_MAGIC = {(byte) 0x1f, (byte) 0x8b}; + + /** + * Indicates whether an InputStream is GZIP compressed. The InputStream must support + * mark()/reset(). + * @param in the InputStream (must return true on markSupported()) + * @return true if the InputStream is GZIP compressed + * @throws IOException in case of an I/O error + */ + public static boolean isGZIPCompressed(InputStream in) throws IOException { + if (!in.markSupported()) { + throw new IllegalArgumentException("InputStream must support mark()!"); + } + byte[] data = new byte[2]; + in.mark(2); + in.read(data); + in.reset(); + return ((data[0] == GZIP_MAGIC[0]) && (data[1] == GZIP_MAGIC[1])); + } + + /** + * Decorates an InputStream with a BufferedInputStream if it doesn't support mark()/reset(). + * @param in the InputStream + * @return the decorated InputStream + */ + public static InputStream decorateMarkSupported(InputStream in) { + if (in.markSupported()) { + return in; + } else { + return new java.io.BufferedInputStream(in); + } + } + + /** + * Automatically decorates an InputStream so it is buffered. Furthermore, it makes sure + * it is decorated with a GZIPInputStream if the stream is GZIP compressed. + * @param in the InputStream + * @return the decorated InputStream + * @throws IOException in case of an I/O error + */ + public static InputStream autoDecorateInputStream(InputStream in) throws IOException { + in = decorateMarkSupported(in); + if (isGZIPCompressed(in)) { + return new GZIPInputStream(in); + } + return in; + } + + /** + * Creates a new hint Map with values from the FOUserAgent. + * @param session the session context + * @return a Map of hints + */ + public static Map getDefaultHints(ImageSessionContext session) { + java.util.Map hints = new java.util.HashMap(); + hints.put(ImageProcessingHints.SOURCE_RESOLUTION, + session.getParentContext().getSourceResolution()); + hints.put(ImageProcessingHints.TARGET_RESOLUTION, + session.getTargetResolution()); + hints.put(ImageProcessingHints.IMAGE_SESSION_CONTEXT, session); + return hints; + } + + private static final String PAGE_INDICATOR = "page="; + + /** + * Extracts page index information from a URI. The expected pattern is "page=x" where x is + * a non-negative integer number. The page index must be specified as part of the URI fragment + * and is 1-based, i.e. the first page is 1 but the the method returns a zero-based page + * index. + * An example: http://www.foo.bar/images/scan1.tif#page=4 (The method will return + * 3.) + *

    + * If no page index information is found in the URI or if the URI cannot be parsed, the + * method returns null. + * @param uri the URI that should be inspected + * @return the page index (0 is the first page) or null if there's no page index information + * in the URI + */ + public static Integer getPageIndexFromURI(String uri) { + if (uri.indexOf('#') < 0) { + return null; + } + try { + URI u = new URI(uri); + String fragment = u.getFragment(); + if (fragment != null) { + int pos = fragment.indexOf(PAGE_INDICATOR); + if (pos >= 0) { + pos += PAGE_INDICATOR.length(); + StringBuffer sb = new StringBuffer(); + while (pos < fragment.length()) { + char c = fragment.charAt(pos); + if (c >= '0' && c <= '9') { + sb.append(c); + } else { + break; + } + pos++; + } + if (sb.length() > 0) { + int pageIndex = Integer.parseInt(sb.toString()) - 1; + pageIndex = Math.max(0, pageIndex); + return pageIndex; + } + } + } + } catch (URISyntaxException e) { + throw new IllegalArgumentException("URI is invalid: " + + e.getLocalizedMessage()); + } + return null; + } + + /** + * Extracts page index information from a URI. The expected pattern is "page=x" where x is + * a non-negative integer number. The page index must be specified as part of the URI fragment + * and is 1-based, i.e. the first page is 1 but the the method returns a zero-based page + * index. + * An example: http://www.foo.bar/images/scan1.tif#page=4 (The method will return + * 3.) + *

    + * If no page index information is found in the URI, the + * method just returns 0 which indicates the first page. + * @param uri the URI that should be inspected + * @return the page index (0 is the first page) + */ + public static int needPageIndexFromURI(String uri) { + Integer res = getPageIndexFromURI(uri); + if (res != null) { + return res; + } else { + return 0; + } + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/util/Penalty.java b/src/main/java/org/apache/xmlgraphics/image/loader/util/Penalty.java new file mode 100644 index 0000000..bddcbb3 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/util/Penalty.java @@ -0,0 +1,106 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: Penalty.java 1345683 2012-06-03 14:50:33Z gadams $ */ + +package org.apache.xmlgraphics.image.loader.util; + +/** + * Immutable class representing a penalty value. It's valid value range is that of an + * {@link Integer}, but giving {@link Integer#MAX_VALUE} a special meaning: it means infinite + * penalty, i.e. a candidate with this penalty will be excluded from any choice. + */ +public final class Penalty { + + public static final Penalty ZERO_PENALTY = new Penalty(0); + public static final Penalty INFINITE_PENALTY = new Penalty(Integer.MAX_VALUE); + + private final int value; + + /** + * Turns a penalty value into a penaly object. + * @param value the penalty value + * @return the penalty object + */ + public static Penalty toPenalty(int value) { + switch (value) { + case 0: + return ZERO_PENALTY; + case Integer.MAX_VALUE: + return INFINITE_PENALTY; + default: + return new Penalty(value); + } + } + + private Penalty(int value) { + this.value = value; + } + + /** + * Adds a penalty to this one and returns the combined penalty. + * @param value the penalty value to add + * @return the resulting penalty + */ + public Penalty add(Penalty value) { + return add(value.getValue()); + } + + /** + * Adds a penalty to this one and returns the combined penalty. + * @param value the penalty value to add + * @return the resulting penalty + */ + public Penalty add(int value) { + long p = (long)getValue() + value; + return toPenalty(truncate(p)); + } + + /** + * Returns the penalty value. + * @return the penalty value + */ + public int getValue() { + return this.value; + } + + /** + * Indicates whether this is an infinite penalty, meaning that a solution with this penalty + * is effectively ineligible. + * @return true if this is an infinite penalty + */ + public boolean isInfinitePenalty() { + return (value == Integer.MAX_VALUE); + } + + /** {@inheritDoc} */ + public String toString() { + return "Penalty: " + (isInfinitePenalty() ? "INF" : Integer.toString(getValue())); + } + + /** + * Truncates the long penalty value to an integer without sign side-effects. + * @param penalty the penalty value as a long + * @return the penalty value as an int + */ + public static int truncate(long penalty) { + penalty = Math.min(Integer.MAX_VALUE, penalty); + penalty = Math.max(Integer.MIN_VALUE, penalty); + return (int)penalty; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/util/SeekableStreamAdapter.java b/src/main/java/org/apache/xmlgraphics/image/loader/util/SeekableStreamAdapter.java new file mode 100644 index 0000000..7aca820 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/util/SeekableStreamAdapter.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: SeekableStreamAdapter.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.image.loader.util; + +import java.io.IOException; + +import javax.imageio.stream.ImageInputStream; + +import org.apache.xmlgraphics.image.codec.util.SeekableStream; + +/** + * Adapter which provides a SeekableStream interface over an ImageInputStream. + */ +public class SeekableStreamAdapter extends SeekableStream { + + private ImageInputStream iin; + + /** + * Main constructor + * @param iin the ImageInputStream to operate on + */ + public SeekableStreamAdapter(ImageInputStream iin) { + this.iin = iin; + } + + /** {@inheritDoc} */ + public long getFilePointer() throws IOException { + return iin.getStreamPosition(); + } + + /** {@inheritDoc} */ + public int read() throws IOException { + return iin.read(); + } + + /** {@inheritDoc} */ + public int read(byte[] b, int off, int len) throws IOException { + return iin.read(b, off, len); + } + + /** {@inheritDoc} */ + public void seek(long pos) throws IOException { + iin.seek(pos); + } + + /** {@inheritDoc} */ + public boolean canSeekBackwards() { + return true; + } + + /** {@inheritDoc} */ + public int skipBytes(int n) throws IOException { + return iin.skipBytes(n); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/util/SoftMapCache.java b/src/main/java/org/apache/xmlgraphics/image/loader/util/SoftMapCache.java new file mode 100644 index 0000000..4e7c672 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/util/SoftMapCache.java @@ -0,0 +1,132 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: SoftMapCache.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.image.loader.util; + +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; +import java.util.Collections; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Provides a simple cache using soft references and storing the values in a Map. The keys into + * the Map are hard references, the values are referenced through soft references. The collected + * values are cleaned up through a ReferenceQueue. + */ +public class SoftMapCache { + + /** logger */ + private static Log log = LogFactory.getLog(SoftMapCache.class); + + private Map map; + private ReferenceQueue refQueue = new ReferenceQueue(); + + /** + * Creates a new soft cache. + * @param synched true if the Map containing the values should by synchronized + */ + public SoftMapCache(boolean synched) { + this.map = new java.util.HashMap(); + if (synched) { + this.map = Collections.synchronizedMap(this.map); + } + } + + /** + * Returns the value associated with the given key. If the value is not found or the value + * has been collected, null is returned. + * @param key the key + * @return the requested value or null + */ + public Object get(Object key) { + Reference ref = (Reference)map.get(key); + return getReference(key, ref); + } + + /** + * Removed the value associated with the given key. The value that is removed is returned + * as the methods result. If the value is not found or the value has been collected, + * null is returned. + * @param key the key + * @return the requested value or null + */ + public Object remove(Object key) { + Reference ref = (Reference)map.remove(key); + return getReference(key, ref); + } + + private Object getReference(Object key, Reference ref) { + Object value = null; + if (ref != null) { + value = ref.get(); + if (value == null) { + //Remove key if its value has been garbage collected + if (log.isTraceEnabled()) { + log.trace("Image has been collected: " + key); + } + checkReferenceQueue(); + } + } + return value; + } + + /** + * Put a new value in the cache overwriting any existing value with the same key. + * @param key The key + * @param value the value + */ + public void put(Object key, Object value) { + map.put(key, wrapInReference(value, key)); + } + + /** + * Clears the cache. + */ + public void clear() { + map.clear(); + } + + /** + * Triggers some house-keeping, i.e. processes any pending objects in the reference queue. + */ + public void doHouseKeeping() { + checkReferenceQueue(); + } + + private Reference wrapInReference(Object obj, Object key) { + return new SoftReferenceWithKey(obj, key, refQueue); + } + + /** + * Checks the reference queue if any references have been cleared and removes them from the + * cache. + */ + private void checkReferenceQueue() { + SoftReferenceWithKey ref; + while ((ref = (SoftReferenceWithKey)refQueue.poll()) != null) { + if (log.isTraceEnabled()) { + log.trace("Removing ImageInfo from ref queue: " + ref.getKey()); + } + map.remove(ref.getKey()); + } + } +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/util/SoftReferenceWithKey.java b/src/main/java/org/apache/xmlgraphics/image/loader/util/SoftReferenceWithKey.java new file mode 100644 index 0000000..ec35631 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/util/SoftReferenceWithKey.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: SoftReferenceWithKey.java 1345683 2012-06-03 14:50:33Z gadams $ */ + +package org.apache.xmlgraphics.image.loader.util; + +import java.lang.ref.ReferenceQueue; +import java.lang.ref.SoftReference; + +/** + * Special SoftReference subclass that holds an additional key object that can be used to remove + * a reference from a Map once the referenced object is collected, for example. + */ +public class SoftReferenceWithKey extends SoftReference { + + private Object key; + + /** + * Creates a new SoftReference with a key. + * @param referent object the new soft reference will refer to + * @param key the key object + * @param q queue the soft reference is registered with + */ + public SoftReferenceWithKey(Object referent, Object key, ReferenceQueue q) { + super(referent, q); + this.key = key; + } + + /** + * Returns the key associated with this reference. + * @return the key + */ + public Object getKey() { + return this.key; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/loader/util/package.html b/src/main/java/org/apache/xmlgraphics/image/loader/util/package.html new file mode 100644 index 0000000..41096b5 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/loader/util/package.html @@ -0,0 +1,26 @@ + + + +org.apache.fop.image2.util Package + +

    + Contains utilities and helper classes useful in conjunction with the image + package. +

    + + \ No newline at end of file diff --git a/src/main/java/org/apache/xmlgraphics/image/package.html b/src/main/java/org/apache/xmlgraphics/image/package.html new file mode 100644 index 0000000..a9d0b1c --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/package.html @@ -0,0 +1,14 @@ + + + + org.apache.xmlgraphics.image + + + +Classes, codecs and utilities for bitmap images. +Contains extensions to the java.awt.image package. This +package provides convenient methods, some utility classes and image +codec classes. These generally bypass broken methods in Java2D or +provide tweaked implementations. + + diff --git a/src/main/java/org/apache/xmlgraphics/image/rendered/AbstractRed.java b/src/main/java/org/apache/xmlgraphics/image/rendered/AbstractRed.java new file mode 100644 index 0000000..ce29b5a --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/rendered/AbstractRed.java @@ -0,0 +1,710 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: AbstractRed.java 1804124 2017-08-04 14:13:54Z ssteiner $ */ + +package org.apache.xmlgraphics.image.rendered; + +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.Shape; +import java.awt.Transparency; +import java.awt.color.ColorSpace; +import java.awt.image.ColorModel; +import java.awt.image.ComponentColorModel; +import java.awt.image.DataBuffer; +import java.awt.image.Raster; +import java.awt.image.RenderedImage; +import java.awt.image.SampleModel; +import java.awt.image.WritableRaster; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Vector; + +import org.apache.xmlgraphics.image.GraphicsUtil; + +// CSOFF: LocalVariableName +// CSOFF: MultipleVariableDeclarations +// CSOFF: NeedBraces +// CSOFF: NoWhitespaceAfter +// CSOFF: WhitespaceAround + +/** + * This is an abstract base class that takes care of most of the + * normal issues surrounding the implementation of the CachableRed + * (RenderedImage) interface. It tries to make no assumptions about + * the subclass implementation. + * + * @version $Id: AbstractRed.java 1804124 2017-08-04 14:13:54Z ssteiner $ + * + * Originally authored by Thomas DeWeese. + */ +public abstract class AbstractRed implements CachableRed { + + protected Rectangle bounds; + protected Vector srcs; + protected Map props; + protected SampleModel sm; + protected ColorModel cm; + protected int tileGridXOff; + protected int tileGridYOff; + protected int tileWidth; + protected int tileHeight; + protected int minTileX; + protected int minTileY; + protected int numXTiles; + protected int numYTiles; + + /** + * void constructor. The subclass must call one of the + * flavors of init before the object becomes usable. + * This is useful when the proper parameters to the init + * method need to be computed in the subclasses constructor. + */ + protected AbstractRed() { + } + + + /** + * Construct an Abstract RenderedImage from a bounds rect and props + * (may be null). The srcs Vector will be empty. + * @param bounds this defines the extent of the rable in the + * user coordinate system. + * @param props this initializes the props Map (may be null) + */ + protected AbstractRed(Rectangle bounds, Map props) { + init((CachableRed)null, bounds, null, null, + bounds.x, bounds.y, props); + } + + /** + * Construct an Abstract RenderedImage from a source image and + * props (may be null). + * @param src will be the first (and only) member of the srcs + * Vector. Src is also used to set the bounds, ColorModel, + * SampleModel, and tile grid offsets. + * @param props this initializes the props Map. */ + protected AbstractRed(CachableRed src, Map props) { + init(src, src.getBounds(), src.getColorModel(), src.getSampleModel(), + src.getTileGridXOffset(), + src.getTileGridYOffset(), + props); + } + + /** + * Construct an Abstract RenderedImage from a source image, bounds + * rect and props (may be null). + * @param src will be the first (and only) member of the srcs + * Vector. Src is also used to set the ColorModel, SampleModel, + * and tile grid offsets. + * @param bounds The bounds of this image. + * @param props this initializes the props Map. */ + protected AbstractRed(CachableRed src, Rectangle bounds, Map props) { + init(src, bounds, src.getColorModel(), src.getSampleModel(), + src.getTileGridXOffset(), + src.getTileGridYOffset(), + props); + } + + /** + * Construct an Abstract RenderedImage from a source image, bounds + * rect and props (may be null). + * @param src if not null, will be the first (and only) member + * of the srcs Vector. Also if it is not null it provides the + * tile grid offsets, otherwise they are zero. + * @param bounds The bounds of this image. + * @param cm The ColorModel to use. If null it will default to + * ComponentColorModel. + * @param sm The sample model to use. If null it will construct + * a sample model the matches the given/generated ColorModel and is + * the size of bounds. + * @param props this initializes the props Map. */ + protected AbstractRed(CachableRed src, Rectangle bounds, + ColorModel cm, SampleModel sm, + Map props) { + init(src, bounds, cm, sm, + (src == null) ? 0 : src.getTileGridXOffset(), + (src == null) ? 0 : src.getTileGridYOffset(), + props); + } + + /** + * Construct an Abstract Rable from a bounds rect and props + * (may be null). The srcs Vector will be empty. + * @param src will be the first (and only) member of the srcs + * Vector. Src is also used to set the ColorModel, SampleModel, + * and tile grid offsets. + * @param bounds this defines the extent of the rable in the + * user coordinate system. + * @param cm The ColorModel to use. If null it will default to + * ComponentColorModel. + * @param sm The sample model to use. If null it will construct + * a sample model the matches the given/generated ColorModel and is + * the size of bounds. + * @param tileGridXOff The x location of tile 0,0. + * @param tileGridYOff The y location of tile 0,0. + * @param props this initializes the props Map. + */ + protected AbstractRed(CachableRed src, Rectangle bounds, + ColorModel cm, SampleModel sm, + int tileGridXOff, int tileGridYOff, + Map props) { + init(src, bounds, cm, sm, tileGridXOff, tileGridYOff, props); + } + + /** + * This is one of two basic init function (this is for single + * source rendereds). + * It is provided so subclasses can compute various values + * before initializing all the state in the base class. + * You really should call this method before returning from + * your subclass constructor. + * + * @param src The source for the filter + * @param bounds The bounds of the image + * @param cm The ColorModel to use. If null it defaults to + * ComponentColorModel/ src's ColorModel. + * @param sm The Sample modle to use. If this is null it will + * use the src's sample model if that is null it will + * construct a sample model that matches the ColorModel + * and is the size of the whole image. + * @param tileGridXOff The x location of tile 0,0. + * @param tileGridYOff The y location of tile 0,0. + * @param props Any properties you want to associate with the image. + */ + protected void init(CachableRed src, Rectangle bounds, + ColorModel cm, SampleModel sm, + int tileGridXOff, int tileGridYOff, + Map props) { + this.srcs = new Vector(1); + if (src != null) { + this.srcs.add(src); + if (bounds == null) { + bounds = src.getBounds(); + } + if (cm == null) { + cm = src.getColorModel(); + } + if (sm == null) { + sm = src.getSampleModel(); + } + } + + this.bounds = bounds; + this.tileGridXOff = tileGridXOff; + this.tileGridYOff = tileGridYOff; + + this.props = new HashMap(); + if (props != null) { + this.props.putAll(props); + } + + if (cm == null) { + cm = new ComponentColorModel( + ColorSpace.getInstance(ColorSpace.CS_GRAY), + new int [] { 8 }, false, false, Transparency.OPAQUE, + DataBuffer.TYPE_BYTE); + } + + this.cm = cm; + + if (sm == null) { + sm = cm.createCompatibleSampleModel(bounds.width, bounds.height); + } + this.sm = sm; + + // Recompute tileWidth/Height, minTileX/Y, numX/YTiles. + updateTileGridInfo(); + } + + /** + * Construct an Abstract Rable from a List of sources a bounds rect + * and props (may be null). + * @param srcs This is used to initialize the srcs Vector. All + * the members of srcs must be CachableRed otherwise an error + * will be thrown. + * @param bounds this defines the extent of the rendered in pixels + * @param props this initializes the props Map. + */ + protected AbstractRed(List srcs, Rectangle bounds, Map props) { + init(srcs, bounds, null, null, bounds.x, bounds.y, props); + } + + /** + * Construct an Abstract RenderedImage from a bounds rect, + * ColorModel (may be null), SampleModel (may be null) and props + * (may be null). The srcs Vector will be empty. + * @param srcs This is used to initialize the srcs Vector. All + * the members of srcs must be CachableRed otherwise an error + * will be thrown. + * @param bounds this defines the extent of the rendered in pixels + * @param cm The ColorModel to use. If null it will default to + * ComponentColorModel. + * @param sm The sample model to use. If null it will construct + * a sample model the matches the given/generated ColorModel and is + * the size of bounds. + * @param props this initializes the props Map. + */ + protected AbstractRed(List srcs, Rectangle bounds, + ColorModel cm, SampleModel sm, + Map props) { + init(srcs, bounds, cm, sm, bounds.x, bounds.y, props); + } + + /** + * Construct an Abstract RenderedImage from a bounds rect, + * ColorModel (may be null), SampleModel (may be null), tile grid + * offsets and props (may be null). The srcs Vector will be + * empty. + * @param srcs This is used to initialize the srcs Vector. All + * the members of srcs must be CachableRed otherwise an error + * will be thrown. + * @param bounds this defines the extent of the rable in the + * user coordinate system. + * @param cm The ColorModel to use. If null it will default to + * ComponentColorModel. + * @param sm The sample model to use. If null it will construct + * a sample model the matches the given/generated ColorModel and is + * the size of bounds. + * @param tileGridXOff The x location of tile 0,0. + * @param tileGridYOff The y location of tile 0,0. + * @param props this initializes the props Map. + */ + protected AbstractRed(List srcs, Rectangle bounds, + ColorModel cm, SampleModel sm, + int tileGridXOff, int tileGridYOff, + Map props) { + init(srcs, bounds, cm, sm, tileGridXOff, tileGridYOff, props); + } + + /** + * This is the basic init function. + * It is provided so subclasses can compute various values + * before initializing all the state in the base class. + * You really should call this method before returning from + * your subclass constructor. + * + * @param srcs The list of sources + * @param bounds The bounds of the image + * @param cm The ColorModel to use. If null it defaults to + * ComponentColorModel. + * @param sm The Sample modle to use. If this is null it will + * construct a sample model that matches the ColorModel + * and is the size of the whole image. + * @param tileGridXOff The x location of tile 0,0. + * @param tileGridYOff The y location of tile 0,0. + * @param props Any properties you want to associate with the image. + */ + protected void init(List srcs, Rectangle bounds, + ColorModel cm, SampleModel sm, + int tileGridXOff, int tileGridYOff, + Map props) { + this.srcs = new Vector(); + if (srcs != null) { + this.srcs.addAll(srcs); + if (srcs.size() != 0) { + CachableRed src = (CachableRed)srcs.get(0); + if (bounds == null) { + bounds = src.getBounds(); + } + if (cm == null) { + cm = src.getColorModel(); + } + if (sm == null) { + sm = src.getSampleModel(); + } + } + } + + this.bounds = bounds; + this.tileGridXOff = tileGridXOff; + this.tileGridYOff = tileGridYOff; + this.props = new HashMap(); + if (props != null) { + this.props.putAll(props); + } + + if (cm == null) { + cm = new ComponentColorModel( + ColorSpace.getInstance(ColorSpace.CS_GRAY), + new int [] { 8 }, false, false, Transparency.OPAQUE, + DataBuffer.TYPE_BYTE); + } + + this.cm = cm; + + if (sm == null) { + sm = cm.createCompatibleSampleModel(bounds.width, bounds.height); + } + this.sm = sm; + + // Recompute tileWidth/Height, minTileX/Y, numX/YTiles. + updateTileGridInfo(); + } + + /** + * This function computes all the basic information about the tile + * grid based on the data stored in sm, and tileGridX/YOff. + * It is responsible for updating tileWidth, tileHeight, + * minTileX/Y, and numX/YTiles. + */ + protected void updateTileGridInfo() { + this.tileWidth = sm.getWidth(); + this.tileHeight = sm.getHeight(); + + int x1; + int y1; + int maxTileX; + int maxTileY; + + // This computes and caches important information about the + // structure of the tile grid in general. + minTileX = getXTile(bounds.x); + minTileY = getYTile(bounds.y); + + x1 = bounds.x + bounds.width - 1; // Xloc of right edge + maxTileX = getXTile(x1); + numXTiles = maxTileX - minTileX + 1; + + y1 = bounds.y + bounds.height - 1; // Yloc of right edge + maxTileY = getYTile(y1); + numYTiles = maxTileY - minTileY + 1; + } + + + public Rectangle getBounds() { + return new Rectangle(getMinX(), + getMinY(), + getWidth(), + getHeight()); + } + + public Vector getSources() { + return srcs; + } + + public ColorModel getColorModel() { + return cm; + } + + public SampleModel getSampleModel() { + return sm; + } + + public int getMinX() { + return bounds.x; + } + public int getMinY() { + return bounds.y; + } + + public int getWidth() { + return bounds.width; + } + + public int getHeight() { + return bounds.height; + } + + public int getTileWidth() { + return tileWidth; + } + + public int getTileHeight() { + return tileHeight; + } + + public int getTileGridXOffset() { + return tileGridXOff; + } + + public int getTileGridYOffset() { + return tileGridYOff; + } + + public int getMinTileX() { + return minTileX; + } + + public int getMinTileY() { + return minTileY; + } + + public int getNumXTiles() { + return numXTiles; + } + + public int getNumYTiles() { + return numYTiles; + } + + public Object getProperty(String name) { + Object ret = props.get(name); + if (ret != null) { + return ret; + } + for (Object src : srcs) { + RenderedImage ri = (RenderedImage) src; + ret = ri.getProperty(name); + if (ret != null) { + return ret; + } + } + return null; + } + + public String [] getPropertyNames() { + Set keys = props.keySet(); + String[] ret = new String[keys.size()]; + keys.toArray(ret); + + for (Object src : srcs) { + RenderedImage ri = (RenderedImage) src; + String[] srcProps = ri.getPropertyNames(); + if (srcProps.length != 0) { + String[] tmp = new String[ret.length + srcProps.length]; + System.arraycopy(ret, 0, tmp, 0, ret.length); + System.arraycopy(srcProps, 0, tmp, ret.length, srcProps.length); + ret = tmp; + } + } + + return ret; + } + + public Shape getDependencyRegion(int srcIndex, Rectangle outputRgn) { + if ((srcIndex < 0) || (srcIndex > srcs.size())) { + throw new IndexOutOfBoundsException( + "Nonexistent source requested."); + } + + // Return empty rect if they don't intersect. + if (!outputRgn.intersects(bounds)) { + return new Rectangle(); + } + + // We only depend on our source for stuff that is inside + // our bounds... + return outputRgn.intersection(bounds); + } + + public Shape getDirtyRegion(int srcIndex, Rectangle inputRgn) { + if (srcIndex != 0) { + throw new IndexOutOfBoundsException( + "Nonexistent source requested."); + } + + // Return empty rect if they don't intersect. + if (!inputRgn.intersects(bounds)) { + return new Rectangle(); + } + + // Changes in the input region don't propogate outside our + // bounds. + return inputRgn.intersection(bounds); + } + + + // This is not included but can be implemented by the following. + // In which case you _must_ reimplement getTile. + // public WritableRaster copyData(WritableRaster wr) { + // copyToRaster(wr); + // return wr; + // } + + public Raster getTile(int tileX, int tileY) { + WritableRaster wr = makeTile(tileX, tileY); + return copyData(wr); + } + + public Raster getData() { + return getData(bounds); + } + + public Raster getData(Rectangle rect) { + SampleModel smRet = sm.createCompatibleSampleModel( + rect.width, rect.height); + + Point pt = new Point(rect.x, rect.y); + WritableRaster wr = Raster.createWritableRaster(smRet, pt); + + // System.out.println("GD DB: " + wr.getDataBuffer().getSize()); + return copyData(wr); + } + + /** + * Returns the x index of tile under xloc. + * @param xloc the x location (in pixels) to get tile for. + * @return The tile index under xloc (may be outside tile grid). + */ + public final int getXTile(int xloc) { + int tgx = xloc - tileGridXOff; + // We need to round to -infinity... + if (tgx >= 0) { + return tgx / tileWidth; + } else { + return (tgx - tileWidth + 1) / tileWidth; + } + } + + /** + * Returns the y index of tile under yloc. + * @param yloc the y location (in pixels) to get tile for. + * @return The tile index under yloc (may be outside tile grid). + */ + public final int getYTile(int yloc) { + int tgy = yloc - tileGridYOff; + // We need to round to -infinity... + if (tgy >= 0) { + return tgy / tileHeight; + } else { + return (tgy - tileHeight + 1) / tileHeight; + } + } + + /** + * Copies data from this images tile grid into wr. wr may + * extend outside the bounds of this image in which case the + * data in wr outside the bounds will not be touched. + * @param wr Raster to fill with image data. + */ + public void copyToRaster(WritableRaster wr) { + int tx0 = getXTile(wr.getMinX()); + int ty0 = getYTile(wr.getMinY()); + int tx1 = getXTile(wr.getMinX() + wr.getWidth() - 1); + int ty1 = getYTile(wr.getMinY() + wr.getHeight() - 1); + + if (tx0 < minTileX) { + tx0 = minTileX; + } + if (ty0 < minTileY) { + ty0 = minTileY; + } + + if (tx1 >= minTileX + numXTiles) { + tx1 = minTileX + numXTiles - 1; + } + if (ty1 >= minTileY + numYTiles) { + ty1 = minTileY + numYTiles - 1; + } + + final boolean isIntPack = + GraphicsUtil.is_INT_PACK_Data(getSampleModel(), false); + + for (int y = ty0; y <= ty1; y++) { + for (int x = tx0; x <= tx1; x++) { + Raster r = getTile(x, y); + if (isIntPack) { + GraphicsUtil.copyData_INT_PACK(r, wr); + } else { + GraphicsUtil.copyData_FALLBACK(r, wr); + } + } + } + } + + + // static DataBufferReclaimer reclaim = new DataBufferReclaimer(); + + /** + * This is a helper function that will create the tile requested + * Including properly subsetting the bounds of the tile to the + * bounds of the current image. + * @param tileX The x index of the tile to be built + * @param tileY The y index of the tile to be built + * @return The tile requested + * @exception IndexOutOfBoundsException if the requested tile index + * falles outside of the bounds of the tile grid for the image. + */ + public WritableRaster makeTile(int tileX, int tileY) { + if ((tileX < minTileX) || (tileX >= minTileX + numXTiles) + || (tileY < minTileY) || (tileY >= minTileY + numYTiles)) { + throw new IndexOutOfBoundsException( + "Requested Tile (" + tileX + ',' + tileY + + ") lies outside the bounds of image"); + } + + Point pt = new Point(tileGridXOff + tileX * tileWidth, + tileGridYOff + tileY * tileHeight); + + WritableRaster wr; + wr = Raster.createWritableRaster(sm, pt); + // if (!(sm instanceof SinglePixelPackedSampleModel)) + // wr = Raster.createWritableRaster(sm, pt); + // else { + // SinglePixelPackedSampleModel sppsm; + // sppsm = (SinglePixelPackedSampleModel)sm; + // int stride = sppsm.getScanlineStride(); + // int sz = stride*sppsm.getHeight(); + // + // int [] data = reclaim.request(sz); + // DataBuffer db = new DataBufferInt(data, sz); + // + // reclaim.register(db); + // + // wr = Raster.createWritableRaster(sm, db, pt); + // } + + // System.out.println("MT DB: " + wr.getDataBuffer().getSize()); + + int x0 = wr.getMinX(); + int y0 = wr.getMinY(); + int x1 = x0 + wr.getWidth() - 1; + int y1 = y0 + wr.getHeight() - 1; + + if ((x0 < bounds.x) || (x1 >= (bounds.x + bounds.width)) + || (y0 < bounds.y) || (y1 >= (bounds.y + bounds.height))) { + // Part of this raster lies outside our bounds so subset + // it so it only advertises the stuff inside our bounds. + if (x0 < bounds.x) { + x0 = bounds.x; + } + if (y0 < bounds.y) { + y0 = bounds.y; + } + if (x1 >= (bounds.x + bounds.width)) { + x1 = bounds.x + bounds.width - 1; + } + if (y1 >= (bounds.y + bounds.height)) { + y1 = bounds.y + bounds.height - 1; + } + + wr = wr.createWritableChild(x0, y0, x1 - x0 + 1, y1 - y0 + 1, + x0, y0, null); + } + return wr; + } + + public static void copyBand(Raster src, int srcBand, + WritableRaster dst, int dstBand) { + Rectangle srcR = new Rectangle(src.getMinX(), src.getMinY(), + src.getWidth(), src.getHeight()); + Rectangle dstR = new Rectangle(dst.getMinX(), dst.getMinY(), + dst.getWidth(), dst.getHeight()); + + Rectangle cpR = srcR.intersection(dstR); + + int [] samples = null; + for (int y = cpR.y; y < cpR.y + cpR.height; y++) { + samples = src.getSamples(cpR.x, y, cpR.width, 1, srcBand, samples); + dst.setSamples(cpR.x, y, cpR.width, 1, dstBand, samples); + } + } +} + diff --git a/src/main/java/org/apache/xmlgraphics/image/rendered/Any2LsRGBRed.java b/src/main/java/org/apache/xmlgraphics/image/rendered/Any2LsRGBRed.java new file mode 100644 index 0000000..386e085 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/rendered/Any2LsRGBRed.java @@ -0,0 +1,316 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: Any2LsRGBRed.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.image.rendered; + +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.color.ColorSpace; +import java.awt.image.BandCombineOp; +import java.awt.image.BufferedImage; +import java.awt.image.ColorConvertOp; +import java.awt.image.ColorModel; +import java.awt.image.DataBuffer; +import java.awt.image.Raster; +import java.awt.image.SampleModel; +import java.awt.image.SinglePixelPackedSampleModel; +import java.awt.image.WritableRaster; + +import org.apache.xmlgraphics.image.GraphicsUtil; + +// CSOFF: ConstantName +// CSOFF: NeedBraces +// CSOFF: WhitespaceAfter +// CSOFF: WhitespaceAround + +/** + * This function will tranform an image from any colorspace into a + * luminance image. The alpha channel if any will be copied to the + * new image. + * + * @version $Id: Any2LsRGBRed.java 1732018 2016-02-24 04:51:06Z gadams $ + * + * Originally authored by Thomas DeWeese. + */ +public class Any2LsRGBRed extends AbstractRed { + + boolean srcIssRGB; + + /** + * Construct a luminace image from src. + * + * @param src The image to convert to a luminance image + */ + public Any2LsRGBRed(CachableRed src) { + super(src, src.getBounds(), + fixColorModel(src), + fixSampleModel(src), + src.getTileGridXOffset(), + src.getTileGridYOffset(), + null); + + ColorModel srcCM = src.getColorModel(); + if (srcCM == null) { + return; + } + ColorSpace srcCS = srcCM.getColorSpace(); + if (srcCS == ColorSpace.getInstance(ColorSpace.CS_sRGB)) { + srcIssRGB = true; + } + } + + /** + * Gamma for linear to sRGB convertion + */ + private static final double GAMMA = 2.4; + private static final double LFACT = 1.0 / 12.92; + + + public static final double sRGBToLsRGB(double value) { + if (value <= 0.003928) { + return value * LFACT; + } + return Math.pow((value + 0.055) / 1.055, GAMMA); + } + + /** + * Lookup tables for RGB lookups. The linearToSRGBLut is used + * when noise values are considered to be on a linearScale. The + * linearToLinear table is used when the values are considered to + * be on the sRGB scale to begin with. + */ + private static final int[] sRGBToLsRGBLut = new int[256]; + static { + final double scale = 1.0 / 255; + + // System.out.print("S2L: "); + for (int i = 0; i < 256; i++) { + double value = sRGBToLsRGB(i * scale); + sRGBToLsRGBLut[i] = (int)Math.round(value * 255.0); + // System.out.print(sRGBToLsRGBLut[i] + ","); + } + // System.out.println(""); + } + + public WritableRaster copyData(WritableRaster wr) { + // Get my source. + CachableRed src = (CachableRed)getSources().get(0); + ColorModel srcCM = src.getColorModel(); + SampleModel srcSM = src.getSampleModel(); + + // Fast case, SRGB source, INT Pack writable raster... + if (srcIssRGB + && Any2sRGBRed.is_INT_PACK_COMP(wr.getSampleModel())) { + src.copyData(wr); + if (srcCM.hasAlpha()) { + GraphicsUtil.coerceData(wr, srcCM, false); + } + Any2sRGBRed.applyLut_INT(wr, sRGBToLsRGBLut); + return wr; + } + + if (srcCM == null) { + // We don't really know much about this source, let's + // guess based on the number of bands... + + float [][] matrix = null; + switch (srcSM.getNumBands()) { + case 1: + matrix = new float[1][3]; + matrix[0][0] = 1; // Red + matrix[0][1] = 1; // Grn + matrix[0][2] = 1; // Blu + break; + case 2: + matrix = new float[2][4]; + matrix[0][0] = 1; // Red + matrix[0][1] = 1; // Grn + matrix[0][2] = 1; // Blu + matrix[1][3] = 1; // Alpha + break; + case 3: + matrix = new float[3][3]; + matrix[0][0] = 1; // Red + matrix[1][1] = 1; // Grn + matrix[2][2] = 1; // Blu + break; + default: + matrix = new float[srcSM.getNumBands()][4]; + matrix[0][0] = 1; // Red + matrix[1][1] = 1; // Grn + matrix[2][2] = 1; // Blu + matrix[3][3] = 1; // Alpha + break; + } + + Raster srcRas = src.getData(wr.getBounds()); + BandCombineOp op = new BandCombineOp(matrix, null); + op.filter(srcRas, wr); + } else { + ColorModel dstCM = getColorModel(); + BufferedImage dstBI; + + if (!dstCM.hasAlpha()) { + // No alpha ao we don't have to work around the bug + // in the color convert op. + dstBI = new BufferedImage( + dstCM, wr.createWritableTranslatedChild(0, 0), + dstCM.isAlphaPremultiplied(), null); + } else { + // All this nonsense is to work around the fact that + // the Color convert op doesn't properly copy the + // Alpha from src to dst. + SinglePixelPackedSampleModel dstSM; + dstSM = (SinglePixelPackedSampleModel)wr.getSampleModel(); + int [] masks = dstSM.getBitMasks(); + SampleModel dstSMNoA = new SinglePixelPackedSampleModel( + dstSM.getDataType(), dstSM.getWidth(), dstSM.getHeight(), + dstSM.getScanlineStride(), + new int[] {masks[0], masks[1], masks[2]}); + ColorModel dstCMNoA = GraphicsUtil.Linear_sRGB; + + WritableRaster dstWr; + dstWr = Raster.createWritableRaster(dstSMNoA, + wr.getDataBuffer(), + new Point(0, 0)); + dstWr = dstWr.createWritableChild( + wr.getMinX() - wr.getSampleModelTranslateX(), + wr.getMinY() - wr.getSampleModelTranslateY(), + wr.getWidth(), wr.getHeight(), + 0, 0, null); + + dstBI = new BufferedImage(dstCMNoA, dstWr, false, null); + } + + // Divide out alpha if we have it. We need to do this since + // the color convert may not be a linear operation which may + // lead to out of range values. + ColorModel srcBICM = srcCM; + WritableRaster srcWr; + if (srcCM.hasAlpha() && srcCM.isAlphaPremultiplied()) { + Rectangle wrR = wr.getBounds(); + SampleModel sm = srcCM.createCompatibleSampleModel( + wrR.width, wrR.height); + + srcWr = Raster.createWritableRaster( + sm, new Point(wrR.x, wrR.y)); + src.copyData(srcWr); + srcBICM = GraphicsUtil.coerceData(srcWr, srcCM, false); + } else { + Raster srcRas = src.getData(wr.getBounds()); + srcWr = GraphicsUtil.makeRasterWritable(srcRas); + } + + BufferedImage srcBI; + srcBI = new BufferedImage(srcBICM, + srcWr.createWritableTranslatedChild(0, 0), + false, + null); + + /* + * System.out.println("src: " + srcBI.getWidth() + "x" + + * srcBI.getHeight()); + * System.out.println("dst: " + dstBI.getWidth() + "x" + + * dstBI.getHeight()); + */ + + ColorConvertOp op = new ColorConvertOp(null); + op.filter(srcBI, dstBI); + + if (dstCM.hasAlpha()) { + copyBand(srcWr, srcSM.getNumBands() - 1, + wr, getSampleModel().getNumBands() - 1); + } + } + return wr; + } + + /** + * This function 'fixes' the source's color model. Right now + * it just selects if it should have one or two bands based on + * if the source had an alpha channel. + */ + protected static ColorModel fixColorModel(CachableRed src) { + ColorModel cm = src.getColorModel(); + if (cm != null) { + if (cm.hasAlpha()) { + return GraphicsUtil.Linear_sRGB_Unpre; + } + + return GraphicsUtil.Linear_sRGB; + } else { + // No ColorModel so try to make some intelligent + // decisions based just on the number of bands... + // 1 bands -> replicated into RGB + // 2 bands -> Band 0 replicated into RGB & Band 1 -> alpha premult + // 3 bands -> sRGB (not-linear?) + // 4 bands -> sRGB premult (not-linear?) + SampleModel sm = src.getSampleModel(); + + switch (sm.getNumBands()) { + case 1: + return GraphicsUtil.Linear_sRGB; + case 2: + return GraphicsUtil.Linear_sRGB_Unpre; + case 3: + return GraphicsUtil.Linear_sRGB; + default: + return GraphicsUtil.Linear_sRGB_Unpre; + } + } + } + + /** + * This function 'fixes' the source's sample model. + * Right now it just selects if it should have 3 or 4 bands + * based on if the source had an alpha channel. + */ + protected static SampleModel fixSampleModel(CachableRed src) { + SampleModel sm = src.getSampleModel(); + ColorModel cm = src.getColorModel(); + + boolean alpha = false; + + if (cm != null) { + alpha = cm.hasAlpha(); + } else { + switch (sm.getNumBands()) { + case 1: case 3: + alpha = false; + break; + default: + alpha = true; + break; + } + } + if (alpha) { + return new SinglePixelPackedSampleModel( + DataBuffer.TYPE_INT, + sm.getWidth(), + sm.getHeight(), + new int [] {0xFF0000, 0xFF00, 0xFF, 0xFF000000}); + } else { + return new SinglePixelPackedSampleModel( + DataBuffer.TYPE_INT, + sm.getWidth(), + sm.getHeight(), + new int [] {0xFF0000, 0xFF00, 0xFF}); + } + } +} diff --git a/src/main/java/org/apache/xmlgraphics/image/rendered/Any2sRGBRed.java b/src/main/java/org/apache/xmlgraphics/image/rendered/Any2sRGBRed.java new file mode 100644 index 0000000..122036b --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/rendered/Any2sRGBRed.java @@ -0,0 +1,400 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: Any2sRGBRed.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.image.rendered; + +import java.awt.color.ColorSpace; +import java.awt.image.BandCombineOp; +import java.awt.image.BufferedImage; +import java.awt.image.ColorConvertOp; +import java.awt.image.ColorModel; +import java.awt.image.DataBuffer; +import java.awt.image.DataBufferInt; +import java.awt.image.Raster; +import java.awt.image.SampleModel; +import java.awt.image.SinglePixelPackedSampleModel; +import java.awt.image.WritableRaster; + +import org.apache.xmlgraphics.image.GraphicsUtil; + +// CSOFF: ConstantName +// CSOFF: MethodName +// CSOFF: MultipleVariableDeclarations +// CSOFF: NeedBraces +// CSOFF: OperatorWrap +// CSOFF: WhitespaceAfter +// CSOFF: WhitespaceAround + +/** + * This function will tranform an image from any colorspace into a + * luminance image. The alpha channel if any will be copied to the + * new image. + * + * @version $Id: Any2sRGBRed.java 1732018 2016-02-24 04:51:06Z gadams $ + * + * Originally authored by Thomas DeWeese. + */ +public class Any2sRGBRed extends AbstractRed { + + boolean srcIsLsRGB; + + /** + * Construct a luminace image from src. + * + * @param src The image to convert to a luminance image + */ + public Any2sRGBRed(CachableRed src) { + super(src, src.getBounds(), + fixColorModel(src), + fixSampleModel(src), + src.getTileGridXOffset(), + src.getTileGridYOffset(), + null); + + ColorModel srcCM = src.getColorModel(); + if (srcCM == null) { + return; + } + ColorSpace srcCS = srcCM.getColorSpace(); + if (srcCS == ColorSpace.getInstance(ColorSpace.CS_LINEAR_RGB)) { + srcIsLsRGB = true; + } + } + + public static boolean is_INT_PACK_COMP(SampleModel sm) { + if (!(sm instanceof SinglePixelPackedSampleModel)) { + return false; + } + + // Check transfer types + if (sm.getDataType() != DataBuffer.TYPE_INT) { + return false; + } + + SinglePixelPackedSampleModel sppsm; + sppsm = (SinglePixelPackedSampleModel)sm; + + int [] masks = sppsm.getBitMasks(); + if ((masks.length != 3) && (masks.length != 4)) { + return false; + } + if (masks[0] != 0x00ff0000) { + return false; + } + if (masks[1] != 0x0000ff00) { + return false; + } + if (masks[2] != 0x000000ff) { + return false; + } + if ((masks.length == 4) + && (masks[3] != 0xff000000)) { + return false; + } + + return true; + } + + /** + * Exponent for linear to sRGB convertion + */ + private static final double GAMMA = 2.4; + + /** + * Lookup tables for RGB lookups. The linearToSRGBLut is used + * when noise values are considered to be on a linearScale. The + * linearToLinear table is used when the values are considered to + * be on the sRGB scale to begin with. + */ + private static final int[] linearToSRGBLut = new int[256]; + static { + final double scale = 1.0 / 255; + final double exp = 1.0 / GAMMA; + // System.out.print("L2S: "); + for (int i = 0; i < 256; i++) { + double value = i * scale; + if (value <= 0.0031308) { + value *= 12.92; + } else { + value = 1.055 * Math.pow(value, exp) - 0.055; + } + + linearToSRGBLut[i] = (int)Math.round(value * 255.); + // System.out.print(linearToSRGBLut[i] + ","); + } + // System.out.println(""); + } + + public static WritableRaster applyLut_INT(WritableRaster wr, + final int []lut) { + SinglePixelPackedSampleModel sm = + (SinglePixelPackedSampleModel)wr.getSampleModel(); + DataBufferInt db = (DataBufferInt)wr.getDataBuffer(); + + final int srcBase + = (db.getOffset() + + sm.getOffset(wr.getMinX() - wr.getSampleModelTranslateX(), + wr.getMinY() - wr.getSampleModelTranslateY())); + // Access the pixel data array + final int[] pixels = db.getBankData()[0]; + final int width = wr.getWidth(); + final int height = wr.getHeight(); + final int scanStride = sm.getScanlineStride(); + + int end; + int pix; + + // For alpha premult we need to multiply all comps. + for (int y = 0; y < height; y++) { + int sp = srcBase + y * scanStride; + end = sp + width; + + while (sp < end) { + pix = pixels[sp]; + pixels[sp] = + ((pix & 0xFF000000) + | (lut[(pix >>> 16) & 0xFF] << 16) + | (lut[(pix >>> 8) & 0xFF] << 8) + | (lut[pix & 0xFF])); + sp++; + } + } + + return wr; + } + + public WritableRaster copyData(WritableRaster wr) { + + // Get my source. + CachableRed src = (CachableRed)getSources().get(0); + ColorModel srcCM = src.getColorModel(); + SampleModel srcSM = src.getSampleModel(); + + + // Fast case, Linear SRGB source, INT Pack writable raster... + if (srcIsLsRGB + && is_INT_PACK_COMP(wr.getSampleModel())) { + src.copyData(wr); + if (srcCM.hasAlpha()) { + GraphicsUtil.coerceData(wr, srcCM, false); + } + applyLut_INT(wr, linearToSRGBLut); + return wr; + } + + if (srcCM == null) { + // We don't really know much about this source, let's + // guess based on the number of bands... + + float [][] matrix = null; + switch (srcSM.getNumBands()) { + case 1: + matrix = new float[3][1]; + matrix[0][0] = 1; // Red + matrix[1][0] = 1; // Grn + matrix[2][0] = 1; // Blu + break; + case 2: + matrix = new float[4][2]; + matrix[0][0] = 1; // Red + matrix[1][0] = 1; // Grn + matrix[3][0] = 1; // Blu + matrix[3][1] = 1; // Alpha + break; + case 3: + matrix = new float[3][3]; + matrix[0][0] = 1; // Red + matrix[1][1] = 1; // Grn + matrix[2][2] = 1; // Blu + break; + default: + matrix = new float[4][srcSM.getNumBands()]; + matrix[0][0] = 1; // Red + matrix[1][1] = 1; // Grn + matrix[2][2] = 1; // Blu + matrix[3][3] = 1; // Alpha + break; + } + Raster srcRas = src.getData(wr.getBounds()); + BandCombineOp op = new BandCombineOp(matrix, null); + op.filter(srcRas, wr); + return wr; + } + + if (srcCM.getColorSpace() + == ColorSpace.getInstance(ColorSpace.CS_GRAY)) { + + // This is a little bit of a hack. There is only + // a linear grayscale ICC profile in the JDK so + // many things use this when the data _really_ + // has sRGB gamma applied. + try { + float [][] matrix = null; + switch (srcSM.getNumBands()) { + case 1: + matrix = new float[3][1]; + matrix[0][0] = 1; // Red + matrix[1][0] = 1; // Grn + matrix[2][0] = 1; // Blu + break; + case 2: + default: + matrix = new float[4][2]; + matrix[0][0] = 1; // Red + matrix[1][0] = 1; // Grn + matrix[3][0] = 1; // Blu + matrix[4][1] = 1; // Alpha + break; + } + Raster srcRas = src.getData(wr.getBounds()); + BandCombineOp op = new BandCombineOp(matrix, null); + op.filter(srcRas, wr); + } catch (Throwable t) { + t.printStackTrace(); + } + return wr; + } + + ColorModel dstCM = getColorModel(); + if (srcCM.getColorSpace() == dstCM.getColorSpace()) { + // No transform needed, just reformat data... + // System.out.println("Bypassing"); + + if (is_INT_PACK_COMP(srcSM)) { + src.copyData(wr); + } else { + GraphicsUtil.copyData(src.getData(wr.getBounds()), wr); + } + + return wr; + } + + Raster srcRas = src.getData(wr.getBounds()); + assert srcRas instanceof WritableRaster; + WritableRaster srcWr = (WritableRaster)srcRas; + + // Divide out alpha if we have it. We need to do this since + // the color convert may not be a linear operation which may + // lead to out of range values. + ColorModel srcBICM = srcCM; + if (srcCM.hasAlpha()) { + srcBICM = GraphicsUtil.coerceData(srcWr, srcCM, false); + } + + BufferedImage srcBI; + BufferedImage dstBI; + srcBI = new BufferedImage(srcBICM, + srcWr.createWritableTranslatedChild(0, 0), + false, + null); + + // System.out.println("src: " + srcBI.getWidth() + "x" + + // srcBI.getHeight()); + + ColorConvertOp op = new ColorConvertOp(dstCM.getColorSpace(), + null); + dstBI = op.filter(srcBI, null); + + // System.out.println("After filter:"); + + WritableRaster wr00 = wr.createWritableTranslatedChild(0, 0); + for (int i = 0; i < dstCM.getColorSpace().getNumComponents(); i++) { + copyBand(dstBI.getRaster(), i, wr00, i); + } + + if (dstCM.hasAlpha()) { + copyBand(srcWr, srcSM.getNumBands() - 1, + wr, getSampleModel().getNumBands() - 1); + } + return wr; + } + + /** + * This function 'fixes' the source's color model. Right now + * it just selects if it should have one or two bands based on + * if the source had an alpha channel. + */ + protected static ColorModel fixColorModel(CachableRed src) { + ColorModel cm = src.getColorModel(); + if (cm != null) { + if (cm.hasAlpha()) { + return GraphicsUtil.sRGB_Unpre; + } + + return GraphicsUtil.sRGB; + } else { + // No ColorModel so try to make some intelligent + // decisions based just on the number of bands... + // 1 bands -> replicated into RGB + // 2 bands -> Band 0 replicated into RGB & Band 1 -> alpha premult + // 3 bands -> sRGB (not-linear?) + // 4 bands -> sRGB premult (not-linear?) + SampleModel sm = src.getSampleModel(); + + switch (sm.getNumBands()) { + case 1: + return GraphicsUtil.sRGB; + case 2: + return GraphicsUtil.sRGB_Unpre; + case 3: + return GraphicsUtil.sRGB; + default: + return GraphicsUtil.sRGB_Unpre; + } + } + } + + /** + * This function 'fixes' the source's sample model. + * Right now it just selects if it should have 3 or 4 bands + * based on if the source had an alpha channel. + */ + protected static SampleModel fixSampleModel(CachableRed src) { + SampleModel sm = src.getSampleModel(); + ColorModel cm = src.getColorModel(); + + boolean alpha = false; + + if (cm != null) { + alpha = cm.hasAlpha(); + } else { + switch (sm.getNumBands()) { + case 1: case 3: + alpha = false; + break; + default: + alpha = true; + break; + } + } + if (alpha) { + return new SinglePixelPackedSampleModel( + DataBuffer.TYPE_INT, + sm.getWidth(), + sm.getHeight(), + new int [] {0xFF0000, 0xFF00, 0xFF, 0xFF000000}); + } else { + return new SinglePixelPackedSampleModel( + DataBuffer.TYPE_INT, + sm.getWidth(), + sm.getHeight(), + new int [] {0xFF0000, 0xFF00, 0xFF}); + } + } +} diff --git a/src/main/java/org/apache/xmlgraphics/image/rendered/BufferedImageCachableRed.java b/src/main/java/org/apache/xmlgraphics/image/rendered/BufferedImageCachableRed.java new file mode 100644 index 0000000..6eeda50 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/rendered/BufferedImageCachableRed.java @@ -0,0 +1,142 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: BufferedImageCachableRed.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.image.rendered; + +import java.awt.Rectangle; +import java.awt.image.BufferedImage; +import java.awt.image.Raster; +import java.awt.image.WritableRaster; + +import org.apache.xmlgraphics.image.GraphicsUtil; + +// CSOFF: NeedBraces +// CSOFF: WhitespaceAfter +// CSOFF: WhitespaceAround + +/** + * This implements CachableRed based on a BufferedImage. + * You can use this to wrap a BufferedImage that you want to + * appear as a CachableRed. + * It essentially ignores the dependency and dirty region methods. + * + * Originally authored by Thomas DeWeese. + * @version $Id: BufferedImageCachableRed.java 1732018 2016-02-24 04:51:06Z gadams $ + */ +public class BufferedImageCachableRed extends AbstractRed { + // The bufferedImage that we wrap... + BufferedImage bi; + + /** + * Construct an instance of CachableRed around a BufferedImage. + */ + public BufferedImageCachableRed(BufferedImage bi) { + super((CachableRed)null, + new Rectangle(bi.getMinX(), bi.getMinY(), + bi.getWidth(), bi.getHeight()), + bi.getColorModel(), bi.getSampleModel(), + bi.getMinX(), bi.getMinY(), null); + + this.bi = bi; + } + + public BufferedImageCachableRed(BufferedImage bi, + int xloc, int yloc) { + super((CachableRed)null, new Rectangle(xloc, yloc, + bi.getWidth(), + bi.getHeight()), + bi.getColorModel(), bi.getSampleModel(), xloc, yloc, null); + + this.bi = bi; + } + + public Rectangle getBounds() { + return new Rectangle(getMinX(), + getMinY(), + getWidth(), + getHeight()); + } + + /** + * fetch the bufferedImage from this node. + */ + public BufferedImage getBufferedImage() { + return bi; + } + + public Object getProperty(String name) { + return bi.getProperty(name); + } + + public String [] getPropertyNames() { + return bi.getPropertyNames(); + } + + public Raster getTile(int tileX, int tileY) { + return bi.getTile(tileX, tileY); + } + + public Raster getData() { + Raster r = bi.getData(); + return r.createTranslatedChild(getMinX(), getMinY()); + } + + public Raster getData(Rectangle rect) { + Rectangle r = (Rectangle)rect.clone(); + + if (!r.intersects(getBounds())) { + return null; + } + r = r.intersection(getBounds()); + r.translate(-getMinX(), -getMinY()); + + Raster ret = bi.getData(r); + return ret.createTranslatedChild(ret.getMinX() + getMinX(), + ret.getMinY() + getMinY()); + } + + public WritableRaster copyData(WritableRaster wr) { + WritableRaster wr2 = wr.createWritableTranslatedChild( + wr.getMinX() - getMinX(), + wr.getMinY() - getMinY()); + + GraphicsUtil.copyData(bi.getRaster(), wr2); + + /* This was the original code. This is _bad_ since it causes a + * multiply and divide of the alpha channel to do the draw + * operation. I believe that at some point I switched to + * drawImage in order to avoid some issues with + * BufferedImage's copyData implementation but I can't + * reproduce them now. Anyway I'm now using GraphicsUtil which + * should generally be as fast if not faster... + */ + /* + BufferedImage dest; + dest = new BufferedImage(bi.getColorModel(), + wr.createWritableTranslatedChild(0,0), + bi.getColorModel().isAlphaPremultiplied(), + null); + java.awt.Graphics2D g2d = dest.createGraphics(); + g2d.drawImage(bi, null, getMinX()-wr.getMinX(), + getMinY()-wr.getMinY()); + g2d.dispose(); + */ + return wr; + } +} diff --git a/src/main/java/org/apache/xmlgraphics/image/rendered/CachableRed.java b/src/main/java/org/apache/xmlgraphics/image/rendered/CachableRed.java new file mode 100644 index 0000000..3bd0454 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/rendered/CachableRed.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: CachableRed.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.image.rendered; + +import java.awt.Rectangle; +import java.awt.Shape; +import java.awt.image.RenderedImage; + +/** + * This provides a number of extra methods that enable a system to + * better analyse the dependencies between nodes in a render graph. + * + * @version $Id: CachableRed.java 1732018 2016-02-24 04:51:06Z gadams $ + * + * Originally authored by Thomas DeWeese. +*/ +public interface CachableRed extends RenderedImage { + + /** + * Returns the bounds of the current image. + * This should be 'in sync' with getMinX, getMinY, getWidth, getHeight + */ + Rectangle getBounds(); + + /** + * Returns the region of input data is is required to generate + * outputRgn. + * @param srcIndex The source to do the dependency calculation for. + * @param outputRgn The region of output you are interested in + * generating dependencies for. The is given in the output pixel + * coordiate system for this node. + * @return The region of input required. This is in the output pixel + * coordinate system for the source indicated by srcIndex. + */ + Shape getDependencyRegion(int srcIndex, Rectangle outputRgn); + + /** + * This calculates the region of output that is affected by a change + * in a region of input. + * @param srcIndex The input that inputRgn reflects changes in. + * @param inputRgn the region of input that has changed, used to + * calculate the returned shape. This is given in the pixel + * coordinate system of the source indicated by srcIndex. + * @return The region of output that would be invalid given + * a change to inputRgn of the source selected by srcIndex. + * this is in the output pixel coordinate system of this node. + */ + Shape getDirtyRegion(int srcIndex, Rectangle inputRgn); +} + diff --git a/src/main/java/org/apache/xmlgraphics/image/rendered/FormatRed.java b/src/main/java/org/apache/xmlgraphics/image/rendered/FormatRed.java new file mode 100644 index 0000000..3bd6036 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/rendered/FormatRed.java @@ -0,0 +1,217 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: FormatRed.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.image.rendered; + +import java.awt.Point; +import java.awt.Transparency; +import java.awt.color.ColorSpace; +import java.awt.image.BufferedImage; +import java.awt.image.ColorModel; +import java.awt.image.ComponentColorModel; +import java.awt.image.ComponentSampleModel; +import java.awt.image.DataBuffer; +import java.awt.image.DirectColorModel; +import java.awt.image.Raster; +import java.awt.image.SampleModel; +import java.awt.image.SinglePixelPackedSampleModel; +import java.awt.image.WritableRaster; + +import org.apache.xmlgraphics.image.GraphicsUtil; + +// CSOFF: NeedBraces +// CSOFF: WhitespaceAfter +// CSOFF: WhitespaceAround + +/** + * This allows you to specify the ColorModel, Alpha premult and/or + * SampleModel to be used for output. If the input image lacks + * Alpha and alpha is included in output then it is filled with + * alpha=1. In all other cases bands are simply copied. + * + * @version $Id: FormatRed.java 1732018 2016-02-24 04:51:06Z gadams $ + * + * Originally authored by Thomas DeWeese. + */ +public class FormatRed extends AbstractRed { + + public static CachableRed construct(CachableRed src, ColorModel cm) { + ColorModel srcCM = src.getColorModel(); + if ((cm.hasAlpha() != srcCM.hasAlpha()) + || (cm.isAlphaPremultiplied() != srcCM.isAlphaPremultiplied())) { + return new FormatRed(src, cm); + } + + if (cm.getNumComponents() != srcCM.getNumComponents()) { + throw new IllegalArgumentException( + "Incompatible ColorModel given"); + } + + + if ((srcCM instanceof ComponentColorModel) + && (cm instanceof ComponentColorModel)) { + return src; + } + + if ((srcCM instanceof DirectColorModel) + && (cm instanceof DirectColorModel)) { + return src; + } + + return new FormatRed(src, cm); + } + + /** + * Construct an instance of CachableRed around a BufferedImage. + */ + public FormatRed(CachableRed cr, SampleModel sm) { + super(cr, cr.getBounds(), + makeColorModel(cr, sm), sm, + cr.getTileGridXOffset(), + cr.getTileGridYOffset(), + null); + } + + public FormatRed(CachableRed cr, ColorModel cm) { + super(cr, cr.getBounds(), + cm, makeSampleModel(cr, cm), + cr.getTileGridXOffset(), + cr.getTileGridYOffset(), + null); + } + + /** + * fetch the source image for this node. + */ + public CachableRed getSource() { + return (CachableRed)getSources().get(0); + } + + public Object getProperty(String name) { + return getSource().getProperty(name); + } + + public String [] getPropertyNames() { + return getSource().getPropertyNames(); + } + + public WritableRaster copyData(WritableRaster wr) { + ColorModel cm = getColorModel(); + CachableRed cr = getSource(); + ColorModel srcCM = cr.getColorModel(); + SampleModel srcSM = cr.getSampleModel(); + srcSM = srcSM.createCompatibleSampleModel(wr.getWidth(), + wr.getHeight()); + WritableRaster srcWR; + srcWR = Raster.createWritableRaster(srcSM, new Point(wr.getMinX(), + wr.getMinY())); + getSource().copyData(srcWR); + + BufferedImage srcBI = new BufferedImage( + srcCM, srcWR.createWritableTranslatedChild(0, 0), + srcCM.isAlphaPremultiplied(), null); + BufferedImage dstBI = new BufferedImage( + cm, wr.createWritableTranslatedChild(0, 0), + cm.isAlphaPremultiplied(), null); + + GraphicsUtil.copyData(srcBI, dstBI); + + return wr; + } + + public static SampleModel makeSampleModel(CachableRed cr, ColorModel cm) { + SampleModel srcSM = cr.getSampleModel(); + return cm.createCompatibleSampleModel(srcSM.getWidth(), + srcSM.getHeight()); + } + + public static ColorModel makeColorModel(CachableRed cr, SampleModel sm) { + ColorModel srcCM = cr.getColorModel(); + ColorSpace cs = srcCM.getColorSpace(); + + int bands = sm.getNumBands(); + + int bits; + int dt = sm.getDataType(); + switch (dt) { + case DataBuffer.TYPE_BYTE: bits = 8; break; + case DataBuffer.TYPE_SHORT: bits = 16; break; + case DataBuffer.TYPE_USHORT: bits = 16; break; + case DataBuffer.TYPE_INT: bits = 32; break; + default: + throw new IllegalArgumentException( + "Unsupported DataBuffer type: " + dt); + } + + boolean hasAlpha = srcCM.hasAlpha(); + if (hasAlpha) { + // if Src has Alpha then our out bands must + // either be one less than the source (no out alpha) + // or equal (still has alpha) + if (bands == srcCM.getNumComponents() - 1) { + hasAlpha = false; + } else if (bands != srcCM.getNumComponents()) { + throw new IllegalArgumentException( + "Incompatible number of bands in and out"); + } + } else { + if (bands == srcCM.getNumComponents() + 1) { + hasAlpha = true; + } else if (bands != srcCM.getNumComponents()) { + throw new IllegalArgumentException( + "Incompatible number of bands in and out"); + } + } + + boolean preMult = srcCM.isAlphaPremultiplied(); + if (!hasAlpha) { + preMult = false; + } + + if (sm instanceof ComponentSampleModel) { + int [] bitsPer = new int[bands]; + for (int i = 0; i < bands; i++) { + bitsPer[i] = bits; + } + + return new ComponentColorModel( + cs, bitsPer, hasAlpha, preMult, + hasAlpha ? Transparency.TRANSLUCENT : Transparency.OPAQUE, + dt); + } else if (sm instanceof SinglePixelPackedSampleModel) { + SinglePixelPackedSampleModel sppsm; + sppsm = (SinglePixelPackedSampleModel)sm; + int[] masks = sppsm.getBitMasks(); + if (bands == 4) { + return new DirectColorModel( + cs, bits, masks[0], masks[1], masks[2], masks[3], + preMult, dt); + } else if (bands == 3) { + return new DirectColorModel( + cs, bits, masks[0], masks[1], masks[2], 0x0, + preMult, dt); + } else { + throw new IllegalArgumentException( + "Incompatible number of bands out for ColorModel"); + } + } + throw new IllegalArgumentException( + "Unsupported SampleModel Type"); + } +} diff --git a/src/main/java/org/apache/xmlgraphics/image/rendered/RenderedImageCachableRed.java b/src/main/java/org/apache/xmlgraphics/image/rendered/RenderedImageCachableRed.java new file mode 100644 index 0000000..6b4df59 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/rendered/RenderedImageCachableRed.java @@ -0,0 +1,162 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: RenderedImageCachableRed.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.image.rendered; + +import java.awt.Rectangle; +import java.awt.Shape; +import java.awt.image.BufferedImage; +import java.awt.image.ColorModel; +import java.awt.image.Raster; +import java.awt.image.RenderedImage; +import java.awt.image.SampleModel; +import java.awt.image.WritableRaster; +import java.util.Vector; + +// CSOFF: NeedBraces +// CSOFF: WhitespaceAround + +/** + * This implements CachableRed around a RenderedImage. + * You can use this to wrap a RenderedImage that you want to + * appear as a CachableRed. + * It essentially ignores the dependency and dirty region methods. + * + * @version $Id: RenderedImageCachableRed.java 1732018 2016-02-24 04:51:06Z gadams $ + * + * Originally authored by Thomas DeWeese. + */ +public class RenderedImageCachableRed implements CachableRed { + + public static CachableRed wrap(RenderedImage ri) { + if (ri instanceof CachableRed) { + return (CachableRed) ri; + } + if (ri instanceof BufferedImage) { + return new BufferedImageCachableRed((BufferedImage)ri); + } + return new RenderedImageCachableRed(ri); + } + + private RenderedImage src; + private Vector srcs = new Vector(0); + + public RenderedImageCachableRed(RenderedImage src) { + if (src == null) { + throw new NullPointerException(); + } + this.src = src; + } + + public Vector getSources() { + return srcs; // should always be empty... + } + + public Rectangle getBounds() { + return new Rectangle(getMinX(), // could we cache the rectangle?? + getMinY(), + getWidth(), + getHeight()); + } + + public int getMinX() { + return src.getMinX(); + } + public int getMinY() { + return src.getMinY(); + } + + public int getWidth() { + return src.getWidth(); + } + public int getHeight() { + return src.getHeight(); + } + + public ColorModel getColorModel() { + return src.getColorModel(); + } + + public SampleModel getSampleModel() { + return src.getSampleModel(); + } + + public int getMinTileX() { + return src.getMinTileX(); + } + public int getMinTileY() { + return src.getMinTileY(); + } + + public int getNumXTiles() { + return src.getNumXTiles(); + } + public int getNumYTiles() { + return src.getNumYTiles(); + } + + public int getTileGridXOffset() { + return src.getTileGridXOffset(); + } + + public int getTileGridYOffset() { + return src.getTileGridYOffset(); + } + + public int getTileWidth() { + return src.getTileWidth(); + } + public int getTileHeight() { + return src.getTileHeight(); + } + + public Object getProperty(String name) { + return src.getProperty(name); + } + + public String[] getPropertyNames() { + return src.getPropertyNames(); + } + + public Raster getTile(int tileX, int tileY) { + return src.getTile(tileX, tileY); + } + + public WritableRaster copyData(WritableRaster raster) { + return src.copyData(raster); + } + + public Raster getData() { + return src.getData(); + } + + public Raster getData(Rectangle rect) { + return src.getData(rect); + } + + public Shape getDependencyRegion(int srcIndex, Rectangle outputRgn) { + throw new IndexOutOfBoundsException( + "Nonexistant source requested."); + } + + public Shape getDirtyRegion(int srcIndex, Rectangle inputRgn) { + throw new IndexOutOfBoundsException( + "Nonexistant source requested."); + } +} diff --git a/src/main/java/org/apache/xmlgraphics/image/rendered/package.html b/src/main/java/org/apache/xmlgraphics/image/rendered/package.html new file mode 100644 index 0000000..4e24230 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/rendered/package.html @@ -0,0 +1,23 @@ + + + +org.apache.xmlgraphics.image.rendered Package + +

    Specialized subclasses of RenderedImage for various tasks (color space conversion, caching etc.)

    + + \ No newline at end of file diff --git a/src/main/java/org/apache/xmlgraphics/image/writer/AbstractImageWriter.java b/src/main/java/org/apache/xmlgraphics/image/writer/AbstractImageWriter.java new file mode 100644 index 0000000..3dd6e77 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/writer/AbstractImageWriter.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: AbstractImageWriter.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.image.writer; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * Abstract base class for ImageWriter implementations. + * + * @version $Id: AbstractImageWriter.java 750418 2009-03-05 11:03:54Z vhennebert $ + */ +public abstract class AbstractImageWriter implements ImageWriter { + + /** + * @see org.apache.xmlgraphics.image.writer.ImageWriter#createMultiImageWriter( + * java.io.OutputStream) + */ + public MultiImageWriter createMultiImageWriter(OutputStream out) + throws IOException { + throw new UnsupportedOperationException("This ImageWriter does not support writing" + + " multiple images to a single image file."); + } + + /** @see org.apache.xmlgraphics.image.writer.ImageWriter#isFunctional() */ + public boolean isFunctional() { + return true; + } + + /** @see org.apache.xmlgraphics.image.writer.ImageWriter#supportsMultiImageWriter() */ + public boolean supportsMultiImageWriter() { + return false; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/writer/Endianness.java b/src/main/java/org/apache/xmlgraphics/image/writer/Endianness.java new file mode 100644 index 0000000..751d35d --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/writer/Endianness.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: Endianness.java 1732019 2016-02-24 05:01:10Z gadams $ */ + +package org.apache.xmlgraphics.image.writer; + + +/** + * Enumeration for specifying the endianness of the image. + * + * @see Wikipedia on Endianness + */ +public enum Endianness { + + /** + * Default endianness. This can be different depending on the output format or is used + * when the image format doesn't allow to specify the endianness. + */ + DEFAULT, + + /** Little endian, least significant byte first, LSB, Intel Format. */ + LITTLE_ENDIAN, + + /** Big endian, most significant byte first, MSB, Motorola Format. */ + BIG_ENDIAN; + + /** + * Translates an endian type specified in the configuration file into the + * equivalent type should one exist. + * @param value The value specified in the configration file + * @return Returns the Endianess object of the found type. If no type matches + * it returns null. + */ + public static Endianness getEndianType(String value) { + if (value != null) { + for (Endianness endianValue : Endianness.values()) { + if (endianValue.toString().equalsIgnoreCase(value)) { + return endianValue; + } + } + } + return null; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/writer/ImageWriter.java b/src/main/java/org/apache/xmlgraphics/image/writer/ImageWriter.java new file mode 100644 index 0000000..4c4e3ca --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/writer/ImageWriter.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageWriter.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.image.writer; + +import java.awt.image.RenderedImage; +import java.io.IOException; +import java.io.OutputStream; + +/** + * Interface which allows image library independent image writing. + * + * @version $Id: ImageWriter.java 1732018 2016-02-24 04:51:06Z gadams $ + */ +public interface ImageWriter { + + /** + * Encodes an image and writes it to an OutputStream. + * @param image the image to be encoded + * @param out the OutputStream to write to + * @throws IOException In case of an /IO problem + */ + void writeImage(RenderedImage image, OutputStream out) + throws IOException; + + /** + * Encodes an image and writes it to an OutputStream. + * @param image the image to be encoded + * @param out the OutputStream to write to + * @param params a parameters object to customize the encoding. + * @throws IOException In case of an /IO problem + */ + void writeImage(RenderedImage image, OutputStream out, + ImageWriterParams params) + throws IOException; + + /** @return the target MIME type supported by this ImageWriter */ + String getMIMEType(); + + /** @return true if the ImageWriter is expected to work properly in the current environment */ + boolean isFunctional(); + + /** @return true if the implemented format supports multiple pages in a single file */ + boolean supportsMultiImageWriter(); + + /** + * Creates a MultiImageWriter instance that lets you put multiple pages into a single file + * if the format supports it. + * @param out the OutputStream to write the image to + * @return the requested MultiImageWriter instance + * @throws IOException In case of an /IO problem + */ + MultiImageWriter createMultiImageWriter(OutputStream out) throws IOException; + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/writer/ImageWriterParams.java b/src/main/java/org/apache/xmlgraphics/image/writer/ImageWriterParams.java new file mode 100644 index 0000000..86cdb70 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/writer/ImageWriterParams.java @@ -0,0 +1,214 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageWriterParams.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.image.writer; + + +/** + * Parameters for the encoder which is accessed through the + * ImageWriter interface. + * + * @version $Id: ImageWriterParams.java 1732018 2016-02-24 04:51:06Z gadams $ + */ +public class ImageWriterParams { + + /** Forces a single strip for the whole image. */ + public static final int SINGLE_STRIP = -1; + /** Used for generating exactly one strip for each row */ + public static final int ONE_ROW_PER_STRIP = 1; + + private Integer xResolution; + private Integer yResolution; + private Float jpegQuality; + private Boolean jpegForceBaseline; + private String compressionMethod; + private ResolutionUnit resolutionUnit = ResolutionUnit.INCH; + private int rowsPerStrip = ONE_ROW_PER_STRIP; + private Endianness endianness = Endianness.DEFAULT; + + /** + * Default constructor. + */ + public ImageWriterParams() { + //nop + } + + /** + * @return true if resolution has been set + */ + public boolean hasResolution() { + return getXResolution() != null && getYResolution() != null; + } + + /** + * @return the image resolution in dpi, or null if undefined + */ + public Integer getResolution() { + return getXResolution(); + } + + /** + * @return the quality value for encoding a JPEG image + * (0.0-1.0), or null if undefined + */ + public Float getJPEGQuality() { + return this.jpegQuality; + } + + /** + * @return true if the baseline quantization table is forced, + * or null if undefined. + */ + public Boolean getJPEGForceBaseline() { + return this.jpegForceBaseline; + } + + /** @return the compression method for encoding the image */ + public String getCompressionMethod() { + return this.compressionMethod; + } + + /** + * Sets the target resolution of the bitmap image to be written + * (sets both the horizontal and vertical resolution to the same value). + * @param resolution the resolution + */ + public void setResolution(int resolution) { + setXResolution(resolution); + setYResolution(resolution); + } + + /** + * Sets the quality setting for encoding JPEG images. + * @param quality the quality setting (0.0-1.0) + * @param forceBaseline force baseline quantization table + */ + public void setJPEGQuality(float quality, boolean forceBaseline) { + this.jpegQuality = quality; + this.jpegForceBaseline = forceBaseline ? Boolean.TRUE : Boolean.FALSE; + } + + /** + * Set the compression method that shall be used to encode the image. + * @param method the compression method + */ + public void setCompressionMethod(String method) { + this.compressionMethod = method; + } + + /** + * Checks if image is single strip (required by some fax processors). + * @return true if one row per strip. + */ + public boolean isSingleStrip() { + return rowsPerStrip == SINGLE_STRIP; + } + + /** + * Convenience method to set rows per strip to single strip, + * otherwise sets to one row per strip. + * @param isSingle true if a single strip shall be produced, false if multiple strips are ok + */ + public void setSingleStrip(boolean isSingle) { + rowsPerStrip = isSingle ? SINGLE_STRIP : ONE_ROW_PER_STRIP; + } + + /** + * Sets the rows per strip (default is one row per strip); + * if set to -1 (single strip), will use height of the current page, + * required by some fax processors. + * @param rowsPerStrip the value to set. + */ + public void setRowsPerStrip(int rowsPerStrip) { + this.rowsPerStrip = rowsPerStrip; + } + + /** + * The number of rows per strip of the TIFF image, default 1. A value of -1 + * indicates a single strip per page will be used and RowsPerStrip will be set + * to image height for the associated page. + * @return the number of rows per strip, default 1. + */ + public int getRowsPerStrip() { + return rowsPerStrip; + } + + /** + * Returns the unit in which resolution values are given (ex. units per inch). + * @return the resolution unit. + */ + public ResolutionUnit getResolutionUnit() { + return resolutionUnit; + } + + /** + * Sets the resolution unit of the image for calculating resolution. + * @param resolutionUnit the resolution unit (inches, centimeters etc.) + */ + public void setResolutionUnit(ResolutionUnit resolutionUnit) { + this.resolutionUnit = resolutionUnit; + } + + /** + * @return the horizontal image resolution in the current resolution unit, or null if undefined + */ + public Integer getXResolution() { + return xResolution; + } + + /** + * Sets the target horizontal resolution of the bitmap image to be written. + * @param resolution the resolution value + */ + public void setXResolution(int resolution) { + xResolution = resolution; + } + + /** + * @return the vertical image resolution in the current resolution unit, or null if undefined + */ + public Integer getYResolution() { + return yResolution; + } + + /** + * Sets the target vertical resolution of the bitmap image to be written. + * @param resolution the resolution value + */ + public void setYResolution(int resolution) { + yResolution = resolution; + } + + /** + * Returns the endianness selected for the image. + * @return the endianness + */ + public Endianness getEndianness() { + return this.endianness; + } + + /** + * Sets the endianness selected for the image. + * @param endianness the endianness + */ + public void setEndianness(Endianness endianness) { + this.endianness = endianness; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/writer/ImageWriterRegistry.java b/src/main/java/org/apache/xmlgraphics/image/writer/ImageWriterRegistry.java new file mode 100644 index 0000000..b957c10 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/writer/ImageWriterRegistry.java @@ -0,0 +1,177 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageWriterRegistry.java 1804124 2017-08-04 14:13:54Z ssteiner $ */ + +package org.apache.xmlgraphics.image.writer; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Properties; + +import org.apache.xmlgraphics.util.Service; + +/** + * Registry for {@link ImageWriter} implementations. + */ +public final class ImageWriterRegistry { + + private static volatile ImageWriterRegistry instance; + + private Map> imageWriterMap + = new java.util.HashMap>(); + private Map preferredOrder; + + /** + * Default constructor. The default preferred order for the image writers is loaded from the + * resources. + */ + public ImageWriterRegistry() { + Properties props = new Properties(); + InputStream in = getClass().getResourceAsStream("default-preferred-order.properties"); + if (in != null) { + try { + try { + props.load(in); + } finally { + in.close(); + } + } catch (IOException ioe) { + throw new RuntimeException( + "Could not load default preferred order due to I/O error: " + + ioe.getMessage()); + } + } + setPreferredOrder(props); + setup(); + } + + /** + * Special constructor. The preferred order for the image writers can be specified as a + * Map (for example a Properties file). The entries of the Map consists of fully qualified + * class or package names as keys and integer numbers as values. Zero (0) is the default + * priority. + * @param preferredOrder the map of order properties used to order the plug-ins + */ + public ImageWriterRegistry(Properties preferredOrder) { + setPreferredOrder(preferredOrder); + setup(); + } + + private void setPreferredOrder(Properties preferredOrder) { + Map order = new java.util.HashMap(); + for (Map.Entry entry : preferredOrder.entrySet()) { + order.put(entry.getKey().toString(), + Integer.parseInt(entry.getValue().toString())); + } + this.preferredOrder = order; + } + + /** @return a singleton instance of the ImageWriterRegistry. */ + public static ImageWriterRegistry getInstance() { + if (instance == null) { + instance = new ImageWriterRegistry(); + } + return instance; + } + + private void setup() { + Iterator iter = Service.providers(ImageWriter.class); + while (iter.hasNext()) { + ImageWriter writer = (ImageWriter)iter.next(); + register(writer); + } + } + + private int getPriority(ImageWriter writer) { + String key = writer.getClass().getName(); + Integer value = preferredOrder.get(key); + while (value == null) { + int pos = key.lastIndexOf("."); + if (pos < 0) { + break; + } + key = key.substring(0, pos); + value = preferredOrder.get(key); + } + return (value != null) ? value : 0; + } + + /** + * Registers a new ImageWriter implementation with the associated priority in the registry. + * Higher priorities get preference over lower priorities. + * @param writer the ImageWriter instance to register. + * @param priority the priority of the writer in the registry. + * @see #register(ImageWriter) + */ + public void register(ImageWriter writer, int priority) { + + String key = writer.getClass().getName(); + // Register the priority to preferredOrder; overwrite original priority if exists + preferredOrder.put(key, priority); + + register(writer); + } + + /** + * Registers a new ImageWriter implementation in the registry. If an ImageWriter for the same + * target MIME type has already been registered, it is placed in an array based on priority. + * @param writer the ImageWriter instance to register. + */ + public synchronized void register(ImageWriter writer) { + List entries = imageWriterMap.get(writer.getMIMEType()); + if (entries == null) { + entries = new java.util.ArrayList(); + imageWriterMap.put(writer.getMIMEType(), entries); + } + + int priority = getPriority(writer); + ListIterator li = entries.listIterator(); + while (li.hasNext()) { + ImageWriter w = li.next(); + if (getPriority(w) < priority) { + li.previous(); + break; + } + } + li.add(writer); + } + + /** + * Returns an ImageWriter that can be used to encode an image to the requested MIME type. + * @param mime the MIME type of the desired output format + * @return a functional ImageWriter instance handling the desired output format or + * null if none can be found. + */ + public synchronized ImageWriter getWriterFor(String mime) { + List entries = imageWriterMap.get(mime); + if (entries == null) { + return null; + } + for (ImageWriter writer : entries) { + if (writer.isFunctional()) { + return writer; + } + } + return null; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/writer/ImageWriterUtil.java b/src/main/java/org/apache/xmlgraphics/image/writer/ImageWriterUtil.java new file mode 100644 index 0000000..a282781 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/writer/ImageWriterUtil.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageWriterUtil.java 1345683 2012-06-03 14:50:33Z gadams $ */ + +package org.apache.xmlgraphics.image.writer; + +import java.awt.image.RenderedImage; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; + +import org.apache.commons.io.IOUtils; + +/** + * Convenience methods around ImageWriter for the most important tasks. + */ +public final class ImageWriterUtil { + + private ImageWriterUtil() { + } + + /** + * Saves a RenderedImage as a PNG file with 96 dpi. + * @param bitmap the bitmap to encode + * @param outputFile the target file + * @throws IOException in case of an I/O problem + */ + public static void saveAsPNG(RenderedImage bitmap, File outputFile) + throws IOException { + saveAsPNG(bitmap, 96, outputFile); + } + + /** + * Saves a RenderedImage as a PNG file. + * @param bitmap the bitmap to encode + * @param resolution the bitmap resolution + * @param outputFile the target file + * @throws IOException in case of an I/O problem + */ + public static void saveAsPNG(RenderedImage bitmap, int resolution, File outputFile) + throws IOException { + saveAsFile(bitmap, resolution, outputFile, "image/png"); + } + + /** + * Saves a RenderedImage as a file. The image format is given through the MIME type + * @param bitmap the bitmap to encode + * @param resolution the bitmap resolution + * @param outputFile the target file + * @param mime the MIME type of the target file + * @throws IOException in case of an I/O problem + */ + public static void saveAsFile(RenderedImage bitmap, + int resolution, File outputFile, String mime) + throws IOException { + OutputStream out = new java.io.FileOutputStream(outputFile); + try { + ImageWriter writer = ImageWriterRegistry.getInstance().getWriterFor(mime); + ImageWriterParams params = new ImageWriterParams(); + params.setResolution(resolution); + writer.writeImage(bitmap, out, params); + } finally { + IOUtils.closeQuietly(out); + } + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/writer/MultiImageWriter.java b/src/main/java/org/apache/xmlgraphics/image/writer/MultiImageWriter.java new file mode 100644 index 0000000..3becc25 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/writer/MultiImageWriter.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: MultiImageWriter.java 1345683 2012-06-03 14:50:33Z gadams $ */ + +package org.apache.xmlgraphics.image.writer; + +import java.awt.image.RenderedImage; +import java.io.IOException; + +/** + * Interface which allows writing multiple images into one image file if the output format + * supports this feature. + * + * @version $Id: MultiImageWriter.java 1345683 2012-06-03 14:50:33Z gadams $ + */ +public interface MultiImageWriter { + + /** + * Encodes an image and writes it to the image file. + * @param image the image to be encoded + * @param params a parameters object to customize the encoding. + * @throws IOException In case of an /IO problem + */ + void writeImage(RenderedImage image, ImageWriterParams params) + throws IOException; + + void close() throws IOException; + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/writer/ResolutionUnit.java b/src/main/java/org/apache/xmlgraphics/image/writer/ResolutionUnit.java new file mode 100644 index 0000000..fc59b12 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/writer/ResolutionUnit.java @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ResolutionUnit.java 1732019 2016-02-24 05:01:10Z gadams $ */ + +package org.apache.xmlgraphics.image.writer; + +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Map; + +/** + * Enumeration for resolution units used by images; 1 for None, 2 for Inch and 3 for Centimeter. + */ +public enum ResolutionUnit { + + /** no resolution unit */ + NONE(1, "None"), + /** units per inch */ + INCH(2, "Inch"), + /** units per centimeter */ + CENTIMETER(3, "Centimeter"); + + //Reverse Lookup Table + private static final Map LOOKUP + = new HashMap(); + + static { + for (ResolutionUnit unit : EnumSet.allOf(ResolutionUnit.class)) { + LOOKUP.put(unit.getValue(), unit); + } + } + + private final int value; + private final String description; + + private ResolutionUnit(int value, String description) { + this.value = value; + this.description = description; + } + + /** + * Retrieves the numeric value of the resolution unit. + * + * @return 1, 2 or 3. + */ + public int getValue() { + return value; + } + + /** + * Retrieves the standard textual description of the resolution unit. + * + * @return None, Inch or Centimeter. + */ + public String getDescription() { + return description; + } + + /** + * Reverse lookup by value. + * + * @param value the numeric value of the resolution unit + * @return the resolution unit + */ + public static ResolutionUnit get(int value) { + return LOOKUP.get(value); + } +} diff --git a/src/main/java/org/apache/xmlgraphics/image/writer/imageio/ImageIOImageWriter.java b/src/main/java/org/apache/xmlgraphics/image/writer/imageio/ImageIOImageWriter.java new file mode 100644 index 0000000..70dea84 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/writer/imageio/ImageIOImageWriter.java @@ -0,0 +1,350 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageIOImageWriter.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.image.writer.imageio; + +import java.awt.image.RenderedImage; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Iterator; + +import javax.imageio.IIOImage; +import javax.imageio.ImageIO; +import javax.imageio.ImageTypeSpecifier; +import javax.imageio.ImageWriteParam; +import javax.imageio.event.IIOWriteWarningListener; +import javax.imageio.metadata.IIOInvalidTreeException; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.metadata.IIOMetadataNode; +import javax.imageio.stream.ImageOutputStream; + +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import org.apache.xmlgraphics.image.writer.ImageWriter; +import org.apache.xmlgraphics.image.writer.ImageWriterParams; +import org.apache.xmlgraphics.image.writer.MultiImageWriter; +import org.apache.xmlgraphics.image.writer.ResolutionUnit; +import org.apache.xmlgraphics.util.UnitConv; + +/** + * ImageWriter implementation that uses Image I/O to write images. + * + * @version $Id: ImageIOImageWriter.java 1732018 2016-02-24 04:51:06Z gadams $ + */ +public class ImageIOImageWriter implements ImageWriter, IIOWriteWarningListener { + + private static final String DIMENSION = "Dimension"; + private static final String VERTICAL_PIXEL_SIZE = "VerticalPixelSize"; + private static final String HORIZONTAL_PIXEL_SIZE = "HorizontalPixelSize"; + + private static final String STANDARD_METADATA_FORMAT = "javax_imageio_1.0"; + + private String targetMIME; + + /** + * Main constructor. + * @param mime the MIME type of the image format + */ + public ImageIOImageWriter(String mime) { + this.targetMIME = mime; + } + + /** {@inheritDoc} */ + public void writeImage(RenderedImage image, OutputStream out) throws IOException { + writeImage(image, out, null); + } + + /** {@inheritDoc} */ + public void writeImage(RenderedImage image, OutputStream out, + ImageWriterParams params) + throws IOException { + javax.imageio.ImageWriter iiowriter = getIIOImageWriter(); + iiowriter.addIIOWriteWarningListener(this); + + ImageOutputStream imgout = ImageIO.createImageOutputStream(out); + try { + + ImageWriteParam iwParam = getDefaultWriteParam(iiowriter, image, params); + + IIOMetadata streamMetadata = createStreamMetadata(iiowriter, iwParam, params); + + ImageTypeSpecifier type; + if (iwParam.getDestinationType() != null) { + type = iwParam.getDestinationType(); + } else { + type = ImageTypeSpecifier.createFromRenderedImage(image); + } + + //Handle metadata + IIOMetadata meta = iiowriter.getDefaultImageMetadata( + type, iwParam); + //meta might be null for some JAI codecs as they don't support metadata + if (params != null && meta != null) { + meta = updateMetadata(image, meta, params); + } + + //Write image + iiowriter.setOutput(imgout); + IIOImage iioimg = new IIOImage(image, null, meta); + iiowriter.write(streamMetadata, iioimg, iwParam); + + } finally { + imgout.close(); + iiowriter.dispose(); + } + } + + /** + * Creates the stream metadata for image. By default, this method returns null which + * causes the default stream metadata to be used. Subclasses can override this to + * supply special stream metadata (see TIFF for an example). + * @param writer the image write + * @param writeParam the ImageIO write parameters + * @param params the ImageWriter write parameters + * @return the stream metadata (or null if no special metadata needs to be produced) + */ + protected IIOMetadata createStreamMetadata(javax.imageio.ImageWriter writer, + ImageWriteParam writeParam, ImageWriterParams params) { + return null; //leave the default + } + + private javax.imageio.ImageWriter getIIOImageWriter() { + Iterator iter = ImageIO.getImageWritersByMIMEType(getMIMEType()); + javax.imageio.ImageWriter iiowriter = null; + if (iter.hasNext()) { + iiowriter = iter.next(); + } + if (iiowriter == null) { + throw new UnsupportedOperationException("No ImageIO codec for writing " + + getMIMEType() + " is available!"); + } + return iiowriter; + } + + /** + * Returns the default write parameters for encoding the image. + * @param iiowriter The IIO ImageWriter that will be used + * @param image the image to be encoded + * @param params the parameters for this writer instance + * @return the IIO ImageWriteParam instance + */ + protected ImageWriteParam getDefaultWriteParam( + javax.imageio.ImageWriter iiowriter, RenderedImage image, + ImageWriterParams params) { + ImageWriteParam param = iiowriter.getDefaultWriteParam(); + //System.err.println("Param: " + params); + if ((params != null) && (params.getCompressionMethod() != null)) { + param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + param.setCompressionType(params.getCompressionMethod()); + } + return param; + } + + /** + * Updates the metadata information based on the parameters to this writer. + * @param image the current image being rendered + * @param meta the metadata + * @param params the parameters + * @return the updated metadata + */ + protected IIOMetadata updateMetadata(RenderedImage image, IIOMetadata meta, + ImageWriterParams params) { + if (meta.isStandardMetadataFormatSupported() && params.getResolution() != null) { + + //NOTE: There are several bugs in ImageIO codecs concerning resolution handling + //http://www.tracemodeler.com/articles/aging-bugs-and-setting-dpi-with-java-image-io/ + + float multiplier = (ResolutionUnit.CENTIMETER == params.getResolutionUnit()) ? 10f : UnitConv.IN2MM; + double pixelWidthInMillimeters = multiplier / params.getXResolution().doubleValue(); + double pixelHeightInMillimeters = multiplier / params.getYResolution().doubleValue(); + + //Try with the right value as per the ImageIO spec + updatePixelSize(meta, pixelWidthInMillimeters, pixelHeightInMillimeters); + + //Check the merge result + double checkMerged = getHorizontalPixelSize(meta); + if (!equals(checkMerged, pixelWidthInMillimeters, 0.00001)) { + //Merging bug in Sun/Oracle JRE encountered + //Try compensation strategy for PNG bug: + //http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=5106305 + double horzValue = 1 / pixelWidthInMillimeters; + double vertValue = 1 / pixelHeightInMillimeters; + updatePixelSize(meta, horzValue, vertValue); + } + } + return meta; + } + + private static boolean equals(double d1, double d2, double maxDelta) { + return Math.abs(d1 - d2) <= maxDelta; + } + + private double getHorizontalPixelSize(IIOMetadata meta) { + double result = 0; + IIOMetadataNode root = (IIOMetadataNode)meta.getAsTree(STANDARD_METADATA_FORMAT); + IIOMetadataNode dim = getChildNode(root, DIMENSION); + if (dim != null) { + IIOMetadataNode horz = getChildNode(dim, HORIZONTAL_PIXEL_SIZE); + if (horz != null) { + result = Double.parseDouble(horz.getAttribute("value")); + } + } + return result; + } + + private void updatePixelSize(IIOMetadata meta, double horzValue, double vertValue) { + IIOMetadataNode root = (IIOMetadataNode)meta.getAsTree(STANDARD_METADATA_FORMAT); + IIOMetadataNode dim = getChildNode(root, DIMENSION); + IIOMetadataNode child; + + child = getChildNode(dim, HORIZONTAL_PIXEL_SIZE); + if (child == null) { + child = new IIOMetadataNode(HORIZONTAL_PIXEL_SIZE); + dim.appendChild(child); + } + child.setAttribute("value", Double.toString(horzValue)); + + child = getChildNode(dim, VERTICAL_PIXEL_SIZE); + if (child == null) { + child = new IIOMetadataNode(VERTICAL_PIXEL_SIZE); + dim.appendChild(child); + } + child.setAttribute("value", Double.toString(vertValue)); + try { + meta.mergeTree(STANDARD_METADATA_FORMAT, root); + } catch (IIOInvalidTreeException e) { + throw new RuntimeException("Cannot update image metadata: " + + e.getMessage()); + } + } + + /** + * Returns a specific metadata child node + * @param n the base node + * @param name the name of the child + * @return the requested child node + */ + protected static IIOMetadataNode getChildNode(Node n, String name) { + NodeList nodes = n.getChildNodes(); + for (int i = 0; i < nodes.getLength(); i++) { + Node child = nodes.item(i); + if (name.equals(child.getNodeName())) { + return (IIOMetadataNode)child; + } + } + return null; + } + + /** {@inheritDoc} */ + public String getMIMEType() { + return this.targetMIME; + } + + /** {@inheritDoc} */ + public boolean isFunctional() { + Iterator iter = ImageIO.getImageWritersByMIMEType(getMIMEType()); + //Only return true if an IIO ImageWriter is available in the current environment + return (iter.hasNext()); + } + + /** {@inheritDoc} */ + public void warningOccurred(javax.imageio.ImageWriter source, + int imageIndex, String warning) { + System.err.println("Problem while writing image using ImageI/O: " + + warning); + } + + /** {@inheritDoc} */ + public MultiImageWriter createMultiImageWriter(OutputStream out) throws IOException { + return new IIOMultiImageWriter(out); + } + + /** {@inheritDoc} */ + public boolean supportsMultiImageWriter() { + javax.imageio.ImageWriter iiowriter = getIIOImageWriter(); + try { + return iiowriter.canWriteSequence(); + } finally { + iiowriter.dispose(); + } + } + + private class IIOMultiImageWriter implements MultiImageWriter { + + private javax.imageio.ImageWriter iiowriter; + private ImageOutputStream imageStream; + private boolean prepared; + + public IIOMultiImageWriter(OutputStream out) throws IOException { + this.iiowriter = getIIOImageWriter(); + if (!iiowriter.canWriteSequence()) { + throw new UnsupportedOperationException("This ImageWriter does not support writing" + + " multiple images to a single image file."); + } + iiowriter.addIIOWriteWarningListener(ImageIOImageWriter.this); + + imageStream = ImageIO.createImageOutputStream(out); + iiowriter.setOutput(imageStream); + + } + + public void writeImage(RenderedImage image, ImageWriterParams params) throws IOException { + if (iiowriter == null) { + throw new IllegalStateException("MultiImageWriter already closed!"); + } + ImageWriteParam iwParam = getDefaultWriteParam(iiowriter, image, params); + + if (!prepared) { + //Only prepare once + IIOMetadata streamMetadata = createStreamMetadata(iiowriter, iwParam, params); + iiowriter.prepareWriteSequence(streamMetadata); + prepared = true; + } + + ImageTypeSpecifier type; + if (iwParam.getDestinationType() != null) { + type = iwParam.getDestinationType(); + } else { + type = ImageTypeSpecifier.createFromRenderedImage(image); + } + + //Handle metadata + IIOMetadata meta = iiowriter.getDefaultImageMetadata( + type, iwParam); + //meta might be null for some JAI codecs as they don't support metadata + if (params != null && meta != null) { + meta = updateMetadata(image, meta, params); + } + + //Write image + IIOImage iioimg = new IIOImage(image, null, meta); + iiowriter.writeToSequence(iioimg, iwParam); + } + + public void close() throws IOException { + imageStream.close(); + imageStream = null; + iiowriter.dispose(); + iiowriter = null; + } + + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/writer/imageio/ImageIOJPEGImageWriter.java b/src/main/java/org/apache/xmlgraphics/image/writer/imageio/ImageIOJPEGImageWriter.java new file mode 100644 index 0000000..cf23a99 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/writer/imageio/ImageIOJPEGImageWriter.java @@ -0,0 +1,151 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageIOJPEGImageWriter.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.image.writer.imageio; + +import java.awt.image.RenderedImage; + +import javax.imageio.ImageWriteParam; +import javax.imageio.ImageWriter; +import javax.imageio.metadata.IIOInvalidTreeException; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.metadata.IIOMetadataNode; +import javax.imageio.plugins.jpeg.JPEGImageWriteParam; + +import org.apache.xmlgraphics.image.writer.ImageWriterParams; + + +/** + * ImageWriter that encodes JPEG images using Image I/O. + * + * @version $Id: ImageIOJPEGImageWriter.java 1732018 2016-02-24 04:51:06Z gadams $ + */ +public class ImageIOJPEGImageWriter extends ImageIOImageWriter { + + private static final String JPEG_NATIVE_FORMAT = "javax_imageio_jpeg_image_1.0"; + + /** + * Main constructor. + */ + public ImageIOJPEGImageWriter() { + super("image/jpeg"); + } + + @Override + protected IIOMetadata updateMetadata(RenderedImage image, IIOMetadata meta, + ImageWriterParams params) { + if (JPEG_NATIVE_FORMAT.equals(meta.getNativeMetadataFormatName())) { + meta = addAdobeTransform(meta); + IIOMetadataNode root = (IIOMetadataNode)meta.getAsTree(JPEG_NATIVE_FORMAT); + IIOMetadataNode jv = getChildNode(root, "JPEGvariety"); + if (jv == null) { + jv = new IIOMetadataNode("JPEGvariety"); + root.appendChild(jv); + } + IIOMetadataNode child; + if (params.getResolution() != null) { + child = getChildNode(jv, "app0JFIF"); + if (child == null) { + child = new IIOMetadataNode("app0JFIF"); + jv.appendChild(child); + } + //JPEG gets special treatment because there seems to be a bug in + //the JPEG codec in ImageIO converting the pixel size incorrectly + //(or not at all) when using standard metadata format. + child.setAttribute("majorVersion", null); + child.setAttribute("minorVersion", null); + switch (params.getResolutionUnit()) { + case INCH: + child.setAttribute("resUnits", "1"); //dots per inch + break; + case CENTIMETER: + child.setAttribute("resUnits", "2"); //dots per cm + break; + default: + child.setAttribute("resUnits", "0"); //no unit + } + child.setAttribute("Xdensity", params.getXResolution().toString()); + child.setAttribute("Ydensity", params.getYResolution().toString()); + child.setAttribute("thumbWidth", null); + child.setAttribute("thumbHeight", null); + } + try { + meta.setFromTree(JPEG_NATIVE_FORMAT, root); + //meta.mergeTree(JPEG_NATIVE_FORMAT, root); + } catch (IIOInvalidTreeException e) { + throw new RuntimeException("Cannot update image metadata: " + + e.getMessage(), e); + } + } + return meta; + } + + private static IIOMetadata addAdobeTransform(IIOMetadata meta) { + // add the adobe transformation (transform 1 -> to YCbCr) + IIOMetadataNode root = (IIOMetadataNode)meta.getAsTree(JPEG_NATIVE_FORMAT); + + IIOMetadataNode markerSequence = getChildNode(root, "markerSequence"); + if (markerSequence == null) { + throw new RuntimeException("Invalid metadata!"); + } + + IIOMetadataNode adobeTransform = getChildNode(markerSequence, "app14Adobe"); + if (adobeTransform == null) { + adobeTransform = new IIOMetadataNode("app14Adobe"); + adobeTransform.setAttribute("transform" , "1"); // convert RGB to YCbCr + adobeTransform.setAttribute("version", "101"); + adobeTransform.setAttribute("flags0", "0"); + adobeTransform.setAttribute("flags1", "0"); + + markerSequence.appendChild(adobeTransform); + } else { + adobeTransform.setAttribute("transform" , "1"); + } + + try { + meta.setFromTree(JPEG_NATIVE_FORMAT, root); + } catch (IIOInvalidTreeException e) { + throw new RuntimeException("Cannot update image metadata: " + + e.getMessage(), e); + } + return meta; + } + + /** {@inheritDoc} */ + @Override + protected ImageWriteParam getDefaultWriteParam( + ImageWriter iiowriter, RenderedImage image, + ImageWriterParams params) { + JPEGImageWriteParam param = new JPEGImageWriteParam(iiowriter.getLocale()); + //ImageTypeSpecifier type = ImageTypeSpecifier.createFromRenderedImage(image); + /* + ImageTypeSpecifier type = new ImageTypeSpecifier( + image.getColorModel(), image.getSampleModel()); + */ + /* didn't work as expected... + ImageTypeSpecifier type = ImageTypeSpecifier.createFromBufferedImageType( + BufferedImage.TYPE_INT_RGB); + param.setDestinationType(type); + param.setSourceBands(new int[] {0, 1, 2}); + */ + return param; + } + + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/writer/imageio/ImageIOPNGImageWriter.java b/src/main/java/org/apache/xmlgraphics/image/writer/imageio/ImageIOPNGImageWriter.java new file mode 100644 index 0000000..230f554 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/writer/imageio/ImageIOPNGImageWriter.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageIOPNGImageWriter.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.image.writer.imageio; + +/** + * ImageWriter that encodes PNG images using Image I/O. + * + * @version $Id: ImageIOPNGImageWriter.java 1732018 2016-02-24 04:51:06Z gadams $ + */ +public class ImageIOPNGImageWriter extends ImageIOImageWriter { + + /** + * Main constructor. + */ + public ImageIOPNGImageWriter() { + super("image/png"); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/writer/imageio/ImageIOTIFFImageWriter.java b/src/main/java/org/apache/xmlgraphics/image/writer/imageio/ImageIOTIFFImageWriter.java new file mode 100644 index 0000000..9544bc1 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/writer/imageio/ImageIOTIFFImageWriter.java @@ -0,0 +1,249 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageIOTIFFImageWriter.java 1833700 2018-06-18 10:08:45Z ssteiner $ */ + +package org.apache.xmlgraphics.image.writer.imageio; + +import java.awt.image.RenderedImage; +import java.util.Arrays; +import java.util.Set; + +import javax.imageio.ImageWriteParam; +import javax.imageio.metadata.IIOInvalidTreeException; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.metadata.IIOMetadataNode; + +import org.w3c.dom.Node; + +import org.apache.xmlgraphics.image.codec.tiff.TIFFImageDecoder; +import org.apache.xmlgraphics.image.writer.Endianness; +import org.apache.xmlgraphics.image.writer.ImageWriterParams; +import org.apache.xmlgraphics.image.writer.ResolutionUnit; + +// CSOFF: MultipleVariableDeclarations + +/** + * ImageWriter that encodes TIFF images using Image I/O. + * + * @version $Id: ImageIOTIFFImageWriter.java 1833700 2018-06-18 10:08:45Z ssteiner $ + */ +public class ImageIOTIFFImageWriter extends ImageIOImageWriter { + + private static final String SUN_TIFF_NATIVE_FORMAT + = "com_sun_media_imageio_plugins_tiff_image_1.0"; + private static final String JAVA_TIFF_NATIVE_FORMAT + = "javax_imageio_tiff_image_1.0"; + private static final String SUN_TIFF_NATIVE_STREAM_FORMAT + = "com_sun_media_imageio_plugins_tiff_stream_1.0"; + private static final String JAVA_TIFF_NATIVE_STREAM_FORMAT + = "javax_imageio_tiff_stream_1.0"; + + /** + * Main constructor. + */ + public ImageIOTIFFImageWriter() { + super("image/tiff"); + } + + /** {@inheritDoc} */ + @Override + protected IIOMetadata updateMetadata(RenderedImage image, IIOMetadata meta, + ImageWriterParams params) { + meta = super.updateMetadata(image, meta, params); + //We set the resolution manually using the native format since it appears that + //it doesn't work properly through the standard metadata. Haven't figured out why + //that happens. + if (params.getResolution() != null) { + if (SUN_TIFF_NATIVE_FORMAT.equals(meta.getNativeMetadataFormatName()) + || JAVA_TIFF_NATIVE_FORMAT.equals(meta.getNativeMetadataFormatName())) { + IIOMetadataNode root = new IIOMetadataNode(meta.getNativeMetadataFormatName()); + IIOMetadataNode ifd = getChildNode(root, "TIFFIFD"); + if (ifd == null) { + ifd = new IIOMetadataNode("TIFFIFD"); + root.appendChild(ifd); + } + ifd.appendChild(createResolutionUnitField(params)); + ifd.appendChild(createResolutionField(TIFFImageDecoder.TIFF_X_RESOLUTION, + "XResolution", params.getXResolution(), params.getResolutionUnit())); + ifd.appendChild(createResolutionField(TIFFImageDecoder.TIFF_Y_RESOLUTION, + "YResolution", params.getYResolution(), params.getResolutionUnit())); + int rows = params.isSingleStrip() ? image.getHeight() : params.getRowsPerStrip(); + ifd.appendChild(createShortMetadataNode(TIFFImageDecoder.TIFF_ROWS_PER_STRIP, + "RowsPerStrip", Integer.toString(rows))); + + try { + meta.mergeTree(meta.getNativeMetadataFormatName(), root); + } catch (IIOInvalidTreeException e) { + throw new RuntimeException("Cannot update image metadata: " + + e.getMessage(), e); + } + } + } + return meta; + } + + //number of pixels in 100 Meters + private static final String DENOMINATOR_CENTIMETER = "/" + (100 * 100); + private static final String DENOMINATOR_INCH = "/" + 1; + + private IIOMetadataNode createResolutionField(int number, String name, + Integer resolution, ResolutionUnit unit) { + + String value; + + if (unit == ResolutionUnit.INCH) { + + value = resolution + DENOMINATOR_INCH; + + } else { + + float pixSzMM = 25.4f / resolution.floatValue(); + int numPix = (int)(((1000 * 100) / pixSzMM) + 0.5); + value = numPix + DENOMINATOR_CENTIMETER; + + } + + return createRationalMetadataNode(number, name, value); + } + + /** + * Generate a TIFFField for resolution unit based on the parameters. + * @param params + * @return the new metadata node + */ + private IIOMetadataNode createResolutionUnitField(ImageWriterParams params) { + return createShortMetadataNode(TIFFImageDecoder.TIFF_RESOLUTION_UNIT, "ResolutionUnit", + Integer.toString(params.getResolutionUnit().getValue()), + params.getResolutionUnit().getDescription()); + } + + /** + * Utility to create a TIFFShort metadata child node of a TIFFShorts node for TIFF metadata. + * + * @param number value of the number attribute of the TIFField + * @param name value of the name attribute of the TIFFField + * @param value value of the value attribute of the TIFFShort + * @return the new metadata node + */ + public static final IIOMetadataNode createShortMetadataNode(int number, + String name, String value) { + + return createShortMetadataNode(number, name, value, null); + } + + /** + * Utility to create a TIFFShort metadata child node of a TIFFShorts node for TIFF metadata. + * + * @param number value of the number attribute of the TIFField + * @param name value of the name attribute of the TIFFField + * @param value value of the value attribute of the TIFFShort + * @param description value of the description attribute of the TIFFShort, ignored if null + * @return the new metadata node + */ + public static final IIOMetadataNode createShortMetadataNode(int number, String name, + String value, String description) { + + IIOMetadataNode field = createMetadataField(number, name); + IIOMetadataNode arrayNode; + IIOMetadataNode valueNode; + arrayNode = new IIOMetadataNode("TIFFShorts"); + field.appendChild(arrayNode); + valueNode = new IIOMetadataNode("TIFFShort"); + valueNode.setAttribute("value", value); + if (description != null) { + valueNode.setAttribute("description", description); + } + arrayNode.appendChild(valueNode); + + return field; + } + + /** + * Utility to create a TIFFRational metadata child node of a TIFFRationals node for + * TIFF metadata. + * + * @param number value of the number attribute of the TIFField + * @param name value of the name attribute of the TIFFField + * @param value value of the value attribute of the TIFFRational + * @return the new metadata node + */ + public static final IIOMetadataNode createRationalMetadataNode(int number, + String name, String value) { + + IIOMetadataNode field = createMetadataField(number, name); + IIOMetadataNode arrayNode; + IIOMetadataNode valueNode; + arrayNode = new IIOMetadataNode("TIFFRationals"); + field.appendChild(arrayNode); + valueNode = new IIOMetadataNode("TIFFRational"); + valueNode.setAttribute("value", value); + arrayNode.appendChild(valueNode); + + return field; + } + + /** + * Utility function to create a base TIFFField node for TIFF metadata. + * + * @param number value of the number attribute of the TIFField + * @param name value of the name attribute of the TIFFField + * @return the new metadata node + */ + public static final IIOMetadataNode createMetadataField(int number, String name) { + + IIOMetadataNode field = new IIOMetadataNode("TIFFField"); + field.setAttribute("number", Integer.toString(number)); + field.setAttribute("name", name); + return field; + } + + /** {@inheritDoc} */ + @Override + protected IIOMetadata createStreamMetadata(javax.imageio.ImageWriter writer, + ImageWriteParam writeParam, ImageWriterParams params) { + Endianness endian = (params != null ? params.getEndianness() : Endianness.DEFAULT); + if (endian == Endianness.DEFAULT || endian == null) { + return super.createStreamMetadata(writer, writeParam, params); + } + + //Try changing the Byte Order + IIOMetadata streamMetadata = writer.getDefaultStreamMetadata(writeParam); + if (streamMetadata != null) { + Set names = new java.util.HashSet( + Arrays.asList(streamMetadata.getMetadataFormatNames())); + setFromTree(names, streamMetadata, endian, SUN_TIFF_NATIVE_STREAM_FORMAT); + setFromTree(names, streamMetadata, endian, JAVA_TIFF_NATIVE_STREAM_FORMAT); + } + return streamMetadata; + } + + private void setFromTree(Set names, IIOMetadata streamMetadata, Endianness endian, String format) { + if (names.contains(format)) { + Node root = streamMetadata.getAsTree(format); + root.getFirstChild().getAttributes().item(0).setNodeValue(endian.toString()); + try { + streamMetadata.setFromTree(format, root); + } catch (IIOInvalidTreeException e) { + //This should not happen since we check if the format is supported. + throw new IllegalStateException( + "Could not replace TIFF stream metadata: " + e.getMessage(), e); + } + } + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/writer/imageio/package.html b/src/main/java/org/apache/xmlgraphics/image/writer/imageio/package.html new file mode 100644 index 0000000..4036235 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/writer/imageio/package.html @@ -0,0 +1,23 @@ + + + +org.apache.xmlgraphics.image.writer.imageio Package + +

    Implementations of ImageWriter using the ImageIO API.

    + + \ No newline at end of file diff --git a/src/main/java/org/apache/xmlgraphics/image/writer/internal/PNGImageWriter.java b/src/main/java/org/apache/xmlgraphics/image/writer/internal/PNGImageWriter.java new file mode 100644 index 0000000..ac04a47 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/writer/internal/PNGImageWriter.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PNGImageWriter.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.image.writer.internal; + +import java.awt.image.RenderedImage; +import java.io.IOException; +import java.io.OutputStream; + +import org.apache.xmlgraphics.image.codec.png.PNGImageEncoder; +import org.apache.xmlgraphics.image.writer.AbstractImageWriter; +import org.apache.xmlgraphics.image.writer.ImageWriterParams; + +/** + * ImageWriter implementation that uses the internal PNG codec to + * write PNG files. + * + * @version $Id: PNGImageWriter.java 1732018 2016-02-24 04:51:06Z gadams $ + */ +public class PNGImageWriter extends AbstractImageWriter { + + /** {@inheritDoc} */ + public void writeImage(RenderedImage image, OutputStream out) + throws IOException { + writeImage(image, out, null); + } + + /** {@inheritDoc} */ + public void writeImage(RenderedImage image, OutputStream out, + ImageWriterParams params) throws IOException { + PNGImageEncoder encoder = new PNGImageEncoder(out, null); + encoder.encode(image); + } + + /** {@inheritDoc} */ + public String getMIMEType() { + return "image/png"; + } +} diff --git a/src/main/java/org/apache/xmlgraphics/image/writer/internal/TIFFImageWriter.java b/src/main/java/org/apache/xmlgraphics/image/writer/internal/TIFFImageWriter.java new file mode 100644 index 0000000..f4043c3 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/writer/internal/TIFFImageWriter.java @@ -0,0 +1,151 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: TIFFImageWriter.java 1681108 2015-05-22 13:26:12Z ssteiner $ */ + +package org.apache.xmlgraphics.image.writer.internal; + +import java.awt.image.RenderedImage; +import java.io.IOException; +import java.io.OutputStream; + +import org.apache.xmlgraphics.image.codec.tiff.CompressionValue; +import org.apache.xmlgraphics.image.codec.tiff.TIFFEncodeParam; +import org.apache.xmlgraphics.image.codec.tiff.TIFFField; +import org.apache.xmlgraphics.image.codec.tiff.TIFFImageDecoder; +import org.apache.xmlgraphics.image.codec.tiff.TIFFImageEncoder; +import org.apache.xmlgraphics.image.writer.AbstractImageWriter; +import org.apache.xmlgraphics.image.writer.ImageWriterParams; +import org.apache.xmlgraphics.image.writer.MultiImageWriter; +import org.apache.xmlgraphics.image.writer.ResolutionUnit; + +/** + * ImageWriter implementation that uses the internal TIFF codec to + * write TIFF files. + * + * @version $Id: TIFFImageWriter.java 1681108 2015-05-22 13:26:12Z ssteiner $ + */ +public class TIFFImageWriter extends AbstractImageWriter { + + /** {@inheritDoc} */ + public void writeImage(RenderedImage image, OutputStream out) + throws IOException { + writeImage(image, out, null); + } + + /** {@inheritDoc} */ + public void writeImage(RenderedImage image, OutputStream out, + ImageWriterParams params) throws IOException { + TIFFEncodeParam encodeParams = createTIFFEncodeParams(params); + TIFFImageEncoder encoder = new TIFFImageEncoder(out, encodeParams); + encoder.encode(image); + } + + private TIFFEncodeParam createTIFFEncodeParams(ImageWriterParams params) { + TIFFEncodeParam encodeParams = new TIFFEncodeParam(); + if (params == null) { + encodeParams.setCompression(CompressionValue.NONE); + } else { + encodeParams.setCompression(CompressionValue.getValue(params.getCompressionMethod())); + + if (params.getResolution() != null) { + int numPixX; + int numPixY; + int denom; + + if (ResolutionUnit.INCH == params.getResolutionUnit()) { + numPixX = params.getXResolution(); + numPixY = params.getYResolution(); + denom = 1; + } else { + // Set target resolution + float pixXSzMM = 25.4f / params.getXResolution().floatValue(); + float pixYSzMM = 25.4f / params.getYResolution().floatValue(); + // num Pixs in 100 Meters + numPixX = (int)(((1000 * 100) / pixXSzMM) + 0.5); + numPixY = (int)(((1000 * 100) / pixYSzMM) + 0.5); + denom = 100 * 100; // Centimeters per 100 Meters; + } + + long [] xRational = {numPixX, denom}; + long [] yRational = {numPixY, denom}; + TIFFField [] fields = { + new TIFFField(TIFFImageDecoder.TIFF_RESOLUTION_UNIT, + TIFFField.TIFF_SHORT, 1, + new char[] {(char)params.getResolutionUnit().getValue()}), + new TIFFField(TIFFImageDecoder.TIFF_X_RESOLUTION, + TIFFField.TIFF_RATIONAL, 1, + new long[][] {xRational}), + new TIFFField(TIFFImageDecoder.TIFF_Y_RESOLUTION, + TIFFField.TIFF_RATIONAL, 1, + new long[][] {yRational}) + }; + encodeParams.setExtraFields(fields); + } + } + return encodeParams; + } + + /** {@inheritDoc} */ + public String getMIMEType() { + return "image/tiff"; + } + + /** {@inheritDoc} */ + @Override + public MultiImageWriter createMultiImageWriter(OutputStream out) throws IOException { + return new TIFFMultiImageWriter(out); + } + + /** {@inheritDoc} */ + @Override + public boolean supportsMultiImageWriter() { + return true; + } + + private class TIFFMultiImageWriter implements MultiImageWriter { + + private OutputStream out; + private TIFFEncodeParam encodeParams; + private TIFFImageEncoder encoder; + private Object context; + + public TIFFMultiImageWriter(OutputStream out) throws IOException { + this.out = out; + } + + public void writeImage(RenderedImage image, ImageWriterParams params) throws IOException { + if (encoder == null) { + encodeParams = createTIFFEncodeParams(params); + encoder = new TIFFImageEncoder(out, encodeParams); + } + context = encoder.encodeMultiple(context, image); + } + + public void close() throws IOException { + if (encoder != null) { + encoder.finishMultiple(context); + } + encoder = null; + encodeParams = null; + out.flush(); + } + + } + + +} diff --git a/src/main/java/org/apache/xmlgraphics/image/writer/internal/package.html b/src/main/java/org/apache/xmlgraphics/image/writer/internal/package.html new file mode 100644 index 0000000..ba6cd51 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/writer/internal/package.html @@ -0,0 +1,23 @@ + + + +org.apache.xmlgraphics.image.writer.internal Package + +

    Implementations of ImageWriter using Commons' own image codecs.

    + + \ No newline at end of file diff --git a/src/main/java/org/apache/xmlgraphics/image/writer/package.html b/src/main/java/org/apache/xmlgraphics/image/writer/package.html new file mode 100644 index 0000000..bdd01b0 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/image/writer/package.html @@ -0,0 +1,23 @@ + + + +org.apache.xmlgraphics.image.writer Package + +

    Abstraction layer with interfaces for writing bitmap images.

    + + \ No newline at end of file diff --git a/src/main/java/org/apache/xmlgraphics/io/Resource.java b/src/main/java/org/apache/xmlgraphics/io/Resource.java new file mode 100644 index 0000000..f6928e2 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/io/Resource.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: Resource.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.io; + +import java.io.FilterInputStream; +import java.io.InputStream; + +/** + * This class represents a resolved resource. The type property is used by XGC to identify the + * resource content. + */ +public class Resource extends FilterInputStream { + + private final String type; + + /** + * @param type resource type + * @param inputStream input stream of the resource + */ + public Resource(String type, InputStream inputStream) { + super(inputStream); + this.type = type; + } + + /** + * Constructs a resource of 'unknown' type. + * + * @param inputStream input stream of the resource + */ + public Resource(InputStream inputStream) { + this("unknown", inputStream); + } + + /** + * @return the resource type + */ + public String getType() { + return this.type; + } +} diff --git a/src/main/java/org/apache/xmlgraphics/io/ResourceResolver.java b/src/main/java/org/apache/xmlgraphics/io/ResourceResolver.java new file mode 100644 index 0000000..08c471e --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/io/ResourceResolver.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ResourceResolver.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.io; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.URI; + +/** + * Implementations of this resource resolver allow XGC users to control the URI resolution + * mechanism. All resource and output stream acquisition goes through this when its implementation + * is given to the org.apache.fop.apps.EnvironmentProfile. + */ +public interface ResourceResolver { + + /** + * Get a resource given the URI pointing to said resource. + * + * @param uri the resource URI + * @return the resource + * @throws IOException if an I/O error occured during resource acquisition + */ + Resource getResource(URI uri) throws IOException; + + /** + * Gets an output stream of a given URI. + * + * @param uri the output stream URI + * @return the output stream + * @throws IOException if an I/O error occured while creating an output stream + */ + OutputStream getOutputStream(URI uri) throws IOException; + +} diff --git a/src/main/java/org/apache/xmlgraphics/io/TempResourceResolver.java b/src/main/java/org/apache/xmlgraphics/io/TempResourceResolver.java new file mode 100644 index 0000000..f602ff9 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/io/TempResourceResolver.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: TempResourceResolver.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.io; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * Implementations of this interface resolve URIs for temporary files used by XGC. The temporary- + * resource URI scheme comes from {@link TempResourceURIGenerator#TMP_SCHEME}. + */ +public interface TempResourceResolver { + + /** + * Get a temporary-resource given the identifier pointing to said resource. + * + * @param id the resource identifier + * @return the resource + * @throws IOException if an I/O error occured during resource acquisition + */ + Resource getResource(String id) throws IOException; + + /** + * Gets an temporary-output stream of a given identifier. + * + * @param id the output stream identifier + * @return the output stream + * @throws IOException if an I/O error occured while creating an output stream + */ + OutputStream getOutputStream(String id) throws IOException; +} diff --git a/src/main/java/org/apache/xmlgraphics/io/TempResourceURIGenerator.java b/src/main/java/org/apache/xmlgraphics/io/TempResourceURIGenerator.java new file mode 100644 index 0000000..7f6d249 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/io/TempResourceURIGenerator.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.xmlgraphics.io; + +import java.net.URI; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Creates a URI for any temporary resource used within XGC. + */ +public final class TempResourceURIGenerator { + + public static final String TMP_SCHEME = "tmp"; + + private final String tempURIPrefix; + + private final AtomicLong counter; + + /** + * @param uriPrefix a prefix used to name the unique URI + */ + public TempResourceURIGenerator(String uriPrefix) { + counter = new AtomicLong(); + tempURIPrefix = URI.create(TMP_SCHEME + ":///" + uriPrefix).toASCIIString(); + } + + /** + * Generate a unique URI for a temporary resource + * @return the URI + */ + public URI generate() { + return URI.create(tempURIPrefix + getUniqueId()); + } + + private String getUniqueId() { + return Long.toHexString(counter.getAndIncrement()); + } + + public static boolean isTempURI(URI uri) { + return TMP_SCHEME.equals(uri.getScheme()); + } +} diff --git a/src/main/java/org/apache/xmlgraphics/io/URIResolverAdapter.java b/src/main/java/org/apache/xmlgraphics/io/URIResolverAdapter.java new file mode 100644 index 0000000..fdd3cab --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/io/URIResolverAdapter.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.xmlgraphics.io; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.net.URL; + +import javax.xml.transform.Source; +import javax.xml.transform.TransformerException; +import javax.xml.transform.URIResolver; + +/** + * An adapter between {@link URIResolver} to {@link ResourceResolver}. This adapter allows users + * to utilize the resolvers from the XML library for resource acquisition. + */ +public class URIResolverAdapter implements ResourceResolver { + + private final URIResolver resolver; + + /** + * @param resolver the desired {@link URIResolver} + */ + public URIResolverAdapter(URIResolver resolver) { + this.resolver = resolver; + } + + /** {@inheritDoc} */ + public Resource getResource(URI uri) throws IOException { + try { + Source src = resolver.resolve(uri.toASCIIString(), null); + InputStream resourceStream = XmlSourceUtil.getInputStream(src); + + if (resourceStream == null) { + URL url = new URL(src.getSystemId()); + resourceStream = url.openStream(); + } + return new Resource(resourceStream); + } catch (TransformerException e) { + throw new IOException(e.getMessage()); + } + } + + /** {@inheritDoc} */ + public OutputStream getOutputStream(URI uri) throws IOException { + try { + Source src = resolver.resolve(uri.toASCIIString(), null); + return new URL(src.getSystemId()).openConnection().getOutputStream(); + } catch (TransformerException te) { + throw new IOException(te.getMessage()); + } + } +} diff --git a/src/main/java/org/apache/xmlgraphics/io/XmlSourceUtil.java b/src/main/java/org/apache/xmlgraphics/io/XmlSourceUtil.java new file mode 100644 index 0000000..bccc02a --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/io/XmlSourceUtil.java @@ -0,0 +1,167 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.xmlgraphics.io; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; + +import javax.xml.transform.Source; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.sax.SAXSource; +import javax.xml.transform.stream.StreamResult; +import javax.xml.transform.stream.StreamSource; + +import org.xml.sax.InputSource; + +import org.apache.commons.io.IOUtils; + +import org.apache.xmlgraphics.image.loader.ImageSource; +import org.apache.xmlgraphics.image.loader.util.ImageInputStreamAdapter; +import org.apache.xmlgraphics.image.loader.util.ImageUtil; + +/** + * A utility class for handling {@link Source} objects, more specficially the streams that back + * the {@link Source}. + */ +public final class XmlSourceUtil { + + private XmlSourceUtil() { + } + + /** + * Returns the {@link InputStream} that is backing the given {@link Source} object. + * + * @param src is backed by an {@link InputStream} + * @return the input stream + */ + public static InputStream getInputStream(Source src) { + try { + if (src instanceof StreamSource) { + return ((StreamSource) src).getInputStream(); + } else if (src instanceof DOMSource) { + ByteArrayOutputStream outStream = new ByteArrayOutputStream(); + StreamResult xmlSource = new StreamResult(outStream); + TransformerFactory.newInstance().newTransformer().transform(src, xmlSource); + return new ByteArrayInputStream(outStream.toByteArray()); + } else if (src instanceof SAXSource) { + return ((SAXSource) src).getInputSource().getByteStream(); + } else if (src instanceof ImageSource) { + return new ImageInputStreamAdapter(((ImageSource) src).getImageInputStream()); + } + } catch (Exception e) { + // TODO: How do we want to handle these? They all come from the TransformerFactory + } + return null; + } + + /** + * Returns the InputStream of a Source object. This method throws an IllegalArgumentException + * if there's no InputStream instance available from the Source object. + * @param src the Source object + * @return the InputStream + */ + public static InputStream needInputStream(Source src) { + InputStream in = getInputStream(src); + if (in != null) { + return in; + } else { + throw new IllegalArgumentException("Source must be a StreamSource with an InputStream" + + " or an ImageSource"); + } + } + + /** + * Indicates whether the Source object has a Reader instance. + * @param src the Source object + * @return true if an Reader is available + */ + public static boolean hasReader(Source src) { + if (src instanceof StreamSource) { + Reader reader = ((StreamSource) src).getReader(); + return (reader != null); + } else if (src instanceof SAXSource) { + InputSource is = ((SAXSource) src).getInputSource(); + if (is != null) { + return (is.getCharacterStream() != null); + } + } + return false; + } + + /** + * Removes any references to InputStreams or Readers from the given Source to prohibit + * accidental/unwanted use by a component further downstream. + * @param src the Source object + */ + public static void removeStreams(Source src) { + if (src instanceof ImageSource) { + ImageSource isrc = (ImageSource) src; + isrc.setImageInputStream(null); + } else if (src instanceof StreamSource) { + StreamSource ssrc = (StreamSource) src; + ssrc.setInputStream(null); + ssrc.setReader(null); + } else if (src instanceof SAXSource) { + InputSource is = ((SAXSource) src).getInputSource(); + if (is != null) { + is.setByteStream(null); + is.setCharacterStream(null); + } + } + } + + /** + * Closes the InputStreams or ImageInputStreams of Source objects. Any exception occurring + * while closing the stream is ignored. + * @param src the Source object + */ + public static void closeQuietly(Source src) { + if (src instanceof StreamSource) { + StreamSource streamSource = (StreamSource) src; + IOUtils.closeQuietly(streamSource.getReader()); + } else if (src instanceof ImageSource) { + if (ImageUtil.getImageInputStream(src) != null) { + try { + ImageUtil.getImageInputStream(src).close(); + } catch (IOException ioe) { + // ignore + } + } + } else if (src instanceof SAXSource) { + InputSource is = ((SAXSource) src).getInputSource(); + if (is != null) { + IOUtils.closeQuietly(is.getByteStream()); + IOUtils.closeQuietly(is.getCharacterStream()); + } + } + removeStreams(src); + } + + /** + * Indicates whether the Source object has an InputStream instance. + * @param src the Source object + * @return true if an InputStream is available + */ + public static boolean hasInputStream(Source src) { + return getInputStream(src) != null; + } +} diff --git a/src/main/java/org/apache/xmlgraphics/java2d/AbstractGraphics2D.java b/src/main/java/org/apache/xmlgraphics/java2d/AbstractGraphics2D.java new file mode 100644 index 0000000..d315045 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/java2d/AbstractGraphics2D.java @@ -0,0 +1,1460 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: AbstractGraphics2D.java 1739071 2016-04-14 12:30:21Z ssteiner $ */ + +package org.apache.xmlgraphics.java2d; + +import java.awt.AlphaComposite; +import java.awt.Color; +import java.awt.Composite; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.Paint; +import java.awt.Polygon; +import java.awt.Rectangle; +import java.awt.RenderingHints; +import java.awt.Shape; +import java.awt.Stroke; +import java.awt.font.FontRenderContext; +import java.awt.font.GlyphVector; +import java.awt.font.TextLayout; +import java.awt.geom.AffineTransform; +import java.awt.geom.Arc2D; +import java.awt.geom.Ellipse2D; +import java.awt.geom.GeneralPath; +import java.awt.geom.Line2D; +import java.awt.geom.NoninvertibleTransformException; +import java.awt.geom.RoundRectangle2D; +import java.awt.image.BufferedImage; +import java.awt.image.BufferedImageOp; +import java.awt.image.ImageObserver; +import java.text.AttributedCharacterIterator; +import java.util.Map; + +// CSOFF: NeedBraces +// CSOFF: ParameterName +// CSOFF: WhitespaceAround + +/** + * This extension of the java.awt.Graphics2D abstract class + * is still abstract, but it implements a lot of the Graphics2D + * method in a way that concrete implementations can reuse. + * + * This class uses a GraphicContext to store the state of + * its various attributes that control the rendering, such as the + * current Font, Paint or clip. + * + * Concrete implementations can focus on implementing the rendering + * methods, such as drawShape. As a convenience, rendering + * methods that can be expressed with other rendering methods (e.g., + * drawRect can be expressed as draw(new Rectangle(..))), + * are implemented by AbstractGraphics2D + * + * @version $Id: AbstractGraphics2D.java 1739071 2016-04-14 12:30:21Z ssteiner $ + * @see org.apache.xmlgraphics.java2d.GraphicContext + * + * Originally authored by Vincent Hardy. + */ +public abstract class AbstractGraphics2D extends Graphics2D implements Cloneable { + /** + * Current state of the Graphic Context. The GraphicsContext + * class manages the state of this Graphics2D graphic context + * attributes. + */ + protected GraphicContext gc; + + /** + * Text handling strategy. + */ + protected boolean textAsShapes; + + /** + * Protection agains infinite recursion + */ + protected boolean inPossibleRecursion; + + /** + * @param textAsShapes if true, all text is turned into shapes in the + * convertion. No text is output. + * + */ + public AbstractGraphics2D(boolean textAsShapes) { + this.textAsShapes = textAsShapes; + } + + /** + * Creates a new AbstractGraphics2D from an existing instance. + * @param g the AbstractGraphics2D whose properties should be copied + */ + public AbstractGraphics2D(AbstractGraphics2D g) { + this.gc = (GraphicContext)g.gc.clone(); + this.gc.validateTransformStack(); + this.textAsShapes = g.textAsShapes; + } + + /** + * Translates the origin of the graphics context to the point + * (xy) in the current coordinate system. + * Modifies this graphics context so that its new origin corresponds + * to the point (xy) in this graphics context's + * original coordinate system. All coordinates used in subsequent + * rendering operations on this graphics context will be relative + * to this new origin. + * @param x the x coordinate. + * @param y the y coordinate. + */ + public void translate(int x, int y) { + gc.translate(x, y); + } + + /** + * Gets this graphics context's current color. + * @return this graphics context's current color. + * @see java.awt.Color + * @see java.awt.Graphics#setColor + */ + public Color getColor() { + return gc.getColor(); + } + + /** + * Sets this graphics context's current color to the specified + * color. All subsequent graphics operations using this graphics + * context use this specified color. + * @param c the new rendering color. + * @see java.awt.Color + * @see java.awt.Graphics#getColor + */ + public void setColor(Color c) { + gc.setColor(c); + } + + /** + * Sets the paint mode of this graphics context to overwrite the + * destination with this graphics context's current color. + * This sets the logical pixel operation function to the paint or + * overwrite mode. All subsequent rendering operations will + * overwrite the destination with the current color. + */ + public void setPaintMode() { + gc.setComposite(AlphaComposite.SrcOver); + } + + /** + * Gets the current font. + * @return this graphics context's current font. + * @see java.awt.Font + * @see java.awt.Graphics#setFont + */ + public Font getFont() { + return gc.getFont(); + } + + /** + * Sets this graphics context's font to the specified font. + * All subsequent text operations using this graphics context + * use this font. + * @param font the font. + * @see java.awt.Graphics#getFont + */ + public void setFont(Font font) { + gc.setFont(font); + } + + /** + * Returns the bounding rectangle of the current clipping area. + * This method refers to the user clip, which is independent of the + * clipping associated with device bounds and window visibility. + * If no clip has previously been set, or if the clip has been + * cleared using setClip(null), this method returns + * null. + * The coordinates in the rectangle are relative to the coordinate + * system origin of this graphics context. + * @return the bounding rectangle of the current clipping area, + * or null if no clip is set. + * @see java.awt.Graphics#getClip + * @see java.awt.Graphics#clipRect + * @see java.awt.Graphics#setClip(int, int, int, int) + * @see java.awt.Graphics#setClip(Shape) + * @since JDK1.1 + */ + public Rectangle getClipBounds() { + return gc.getClipBounds(); + } + + + /** + * Intersects the current clip with the specified rectangle. + * The resulting clipping area is the intersection of the current + * clipping area and the specified rectangle. If there is no + * current clipping area, either because the clip has never been + * set, or the clip has been cleared using setClip(null), + * the specified rectangle becomes the new clip. + * This method sets the user clip, which is independent of the + * clipping associated with device bounds and window visibility. + * This method can only be used to make the current clip smaller. + * To set the current clip larger, use any of the setClip methods. + * Rendering operations have no effect outside of the clipping area. + * @param x the x coordinate of the rectangle to intersect the clip with + * @param y the y coordinate of the rectangle to intersect the clip with + * @param width the width of the rectangle to intersect the clip with + * @param height the height of the rectangle to intersect the clip with + * @see #setClip(int, int, int, int) + * @see #setClip(Shape) + */ + public void clipRect(int x, int y, int width, int height) { + gc.clipRect(x, y, width, height); + } + + + /** + * Sets the current clip to the rectangle specified by the given + * coordinates. This method sets the user clip, which is + * independent of the clipping associated with device bounds + * and window visibility. + * Rendering operations have no effect outside of the clipping area. + * @param x the x coordinate of the new clip rectangle. + * @param y the y coordinate of the new clip rectangle. + * @param width the width of the new clip rectangle. + * @param height the height of the new clip rectangle. + * @see java.awt.Graphics#clipRect + * @see java.awt.Graphics#setClip(Shape) + * @since JDK1.1 + */ + public void setClip(int x, int y, int width, int height) { + gc.setClip(x, y, width, height); + } + + + /** + * Gets the current clipping area. + * This method returns the user clip, which is independent of the + * clipping associated with device bounds and window visibility. + * If no clip has previously been set, or if the clip has been + * cleared using setClip(null), this method returns + * null. + * @return a Shape object representing the + * current clipping area, or null if + * no clip is set. + * @see java.awt.Graphics#getClipBounds() + * @see java.awt.Graphics#clipRect(int, int, int, int) + * @see java.awt.Graphics#setClip(int, int, int, int) + * @see java.awt.Graphics#setClip(Shape) + * @since JDK1.1 + */ + public Shape getClip() { + return gc.getClip(); + } + + + /** + * Sets the current clipping area to an arbitrary clip shape. + * Not all objects that implement the Shape + * interface can be used to set the clip. The only + * Shape objects that are guaranteed to be + * supported are Shape objects that are + * obtained via the getClip method and via + * Rectangle objects. This method sets the + * user clip, which is independent of the clipping associated + * with device bounds and window visibility. + * @param clip the Shape to use to set the clip + * @see java.awt.Graphics#getClip() + * @see java.awt.Graphics#clipRect + * @see java.awt.Graphics#setClip(int, int, int, int) + * @since JDK1.1 + */ + public void setClip(Shape clip) { + gc.setClip(clip); + } + + + /** + * Draws a line, using the current color, between the points + * (x1, y1) and (x2, y2) + * in this graphics context's coordinate system. + * @param x1 the first point's x coordinate. + * @param y1 the first point's y coordinate. + * @param x2 the second point's x coordinate. + * @param y2 the second point's y coordinate. + */ + public void drawLine(int x1, int y1, int x2, int y2) { + Line2D line = new Line2D.Float(x1, y1, x2, y2); + draw(line); + } + + + /** + * Fills the specified rectangle. + * The left and right edges of the rectangle are at + * x and x + width - 1. + * The top and bottom edges are at + * y and y + height - 1. + * The resulting rectangle covers an area + * width pixels wide by + * height pixels tall. + * The rectangle is filled using the graphics context's current color. + * @param x the x coordinate + * of the rectangle to be filled. + * @param y the y coordinate + * of the rectangle to be filled. + * @param width the width of the rectangle to be filled. + * @param height the height of the rectangle to be filled. + * @see java.awt.Graphics#clearRect + * @see java.awt.Graphics#drawRect + */ + public void fillRect(int x, int y, int width, int height) { + Rectangle rect = new Rectangle(x, y, width, height); + fill(rect); + } + + public void drawRect(int x, int y, int width, int height) { + Rectangle rect = new Rectangle(x, y, width, height); + draw(rect); + } + + + + /** + * Clears the specified rectangle by filling it with the background + * color of the current drawing surface. This operation does not + * use the current paint mode. + *

    + * Beginning with Java 1.1, the background color + * of offscreen images may be system dependent. Applications should + * use setColor followed by fillRect to + * ensure that an offscreen image is cleared to a specific color. + * @param x the x coordinate of the rectangle to clear. + * @param y the y coordinate of the rectangle to clear. + * @param width the width of the rectangle to clear. + * @param height the height of the rectangle to clear. + * @see java.awt.Graphics#fillRect(int, int, int, int) + * @see java.awt.Graphics#drawRect + * @see java.awt.Graphics#setColor(java.awt.Color) + * @see java.awt.Graphics#setPaintMode + * @see java.awt.Graphics#setXORMode(java.awt.Color) + */ + public void clearRect(int x, int y, int width, int height) { + Paint paint = gc.getPaint(); + gc.setColor(gc.getBackground()); + fillRect(x, y, width, height); + gc.setPaint(paint); + } + + /** + * Draws an outlined round-cornered rectangle using this graphics + * context's current color. The left and right edges of the rectangle + * are at x and x + width, + * respectively. The top and bottom edges of the rectangle are at + * y and y + height. + * @param x the x coordinate of the rectangle to be drawn. + * @param y the y coordinate of the rectangle to be drawn. + * @param width the width of the rectangle to be drawn. + * @param height the height of the rectangle to be drawn. + * @param arcWidth the horizontal diameter of the arc + * at the four corners. + * @param arcHeight the vertical diameter of the arc + * at the four corners. + * @see java.awt.Graphics#fillRoundRect + */ + public void drawRoundRect(int x, int y, int width, int height, + int arcWidth, int arcHeight) { + RoundRectangle2D rect = new RoundRectangle2D.Float(x, y, width, height, arcWidth, arcHeight); + draw(rect); + } + + + /** + * Fills the specified rounded corner rectangle with the current color. + * The left and right edges of the rectangle + * are at x and x + width - 1, + * respectively. The top and bottom edges of the rectangle are at + * y and y + height - 1. + * @param x the x coordinate of the rectangle to be filled. + * @param y the y coordinate of the rectangle to be filled. + * @param width the width of the rectangle to be filled. + * @param height the height of the rectangle to be filled. + * @param arcWidth the horizontal diameter + * of the arc at the four corners. + * @param arcHeight the vertical diameter + * of the arc at the four corners. + * @see java.awt.Graphics#drawRoundRect + */ + public void fillRoundRect(int x, int y, int width, int height, + int arcWidth, int arcHeight) { + RoundRectangle2D rect = new RoundRectangle2D.Float(x, y, width, height, arcWidth, arcHeight); + fill(rect); + } + + + /** + * Draws the outline of an oval. + * The result is a circle or ellipse that fits within the + * rectangle specified by the x, y, + * width, and height arguments. + *

    + * The oval covers an area that is + * width + 1 pixels wide + * and height + 1 pixels tall. + * @param x the x coordinate of the upper left + * corner of the oval to be drawn. + * @param y the y coordinate of the upper left + * corner of the oval to be drawn. + * @param width the width of the oval to be drawn. + * @param height the height of the oval to be drawn. + * @see java.awt.Graphics#fillOval + */ + public void drawOval(int x, int y, int width, int height) { + Ellipse2D oval = new Ellipse2D.Float(x, y, width, height); + draw(oval); + } + + + /** + * Fills an oval bounded by the specified rectangle with the + * current color. + * @param x the x coordinate of the upper left corner + * of the oval to be filled. + * @param y the y coordinate of the upper left corner + * of the oval to be filled. + * @param width the width of the oval to be filled. + * @param height the height of the oval to be filled. + * @see java.awt.Graphics#drawOval + */ + public void fillOval(int x, int y, int width, int height) { + Ellipse2D oval = new Ellipse2D.Float(x, y, width, height); + fill(oval); + } + + + /** + * Draws the outline of a circular or elliptical arc + * covering the specified rectangle. + *

    + * The resulting arc begins at startAngle and extends + * for arcAngle degrees, using the current color. + * Angles are interpreted such that 0 degrees + * is at the 3 o'clock position. + * A positive value indicates a counter-clockwise rotation + * while a negative value indicates a clockwise rotation. + *

    + * The center of the arc is the center of the rectangle whose origin + * is (xy) and whose size is specified by the + * width and height arguments. + *

    + * The resulting arc covers an area + * width + 1 pixels wide + * by height + 1 pixels tall. + *

    + * The angles are specified relative to the non-square extents of + * the bounding rectangle such that 45 degrees always falls on the + * line from the center of the ellipse to the upper right corner of + * the bounding rectangle. As a result, if the bounding rectangle is + * noticeably longer in one axis than the other, the angles to the + * start and end of the arc segment will be skewed farther along the + * longer axis of the bounds. + * @param x the x coordinate of the + * upper-left corner of the arc to be drawn. + * @param y the y coordinate of the + * upper-left corner of the arc to be drawn. + * @param width the width of the arc to be drawn. + * @param height the height of the arc to be drawn. + * @param startAngle the beginning angle. + * @param arcAngle the angular extent of the arc, + * relative to the start angle. + * @see java.awt.Graphics#fillArc + */ + public void drawArc(int x, int y, int width, int height, + int startAngle, int arcAngle) { + Arc2D arc = new Arc2D.Float(x, y, width, height, startAngle, arcAngle, Arc2D.OPEN); + draw(arc); + } + + + /** + * Fills a circular or elliptical arc covering the specified rectangle. + *

    + * The resulting arc begins at startAngle and extends + * for arcAngle degrees. + * Angles are interpreted such that 0 degrees + * is at the 3 o'clock position. + * A positive value indicates a counter-clockwise rotation + * while a negative value indicates a clockwise rotation. + *

    + * The center of the arc is the center of the rectangle whose origin + * is (xy) and whose size is specified by the + * width and height arguments. + *

    + * The resulting arc covers an area + * width + 1 pixels wide + * by height + 1 pixels tall. + *

    + * The angles are specified relative to the non-square extents of + * the bounding rectangle such that 45 degrees always falls on the + * line from the center of the ellipse to the upper right corner of + * the bounding rectangle. As a result, if the bounding rectangle is + * noticeably longer in one axis than the other, the angles to the + * start and end of the arc segment will be skewed farther along the + * longer axis of the bounds. + * @param x the x coordinate of the + * upper-left corner of the arc to be filled. + * @param y the y coordinate of the + * upper-left corner of the arc to be filled. + * @param width the width of the arc to be filled. + * @param height the height of the arc to be filled. + * @param startAngle the beginning angle. + * @param arcAngle the angular extent of the arc, + * relative to the start angle. + * @see java.awt.Graphics#drawArc + */ + public void fillArc(int x, int y, int width, int height, + int startAngle, int arcAngle) { + Arc2D arc = new Arc2D.Float(x, y, width, height, startAngle, arcAngle, Arc2D.PIE); + fill(arc); + } + + + /** + * Draws a sequence of connected lines defined by + * arrays of x and y coordinates. + * Each pair of (xy) coordinates defines a point. + * The figure is not closed if the first point + * differs from the last point. + * @param xPoints an array of x points + * @param yPoints an array of y points + * @param nPoints the total number of points + * @see java.awt.Graphics#drawPolygon(int[], int[], int) + * @since JDK1.1 + */ + public void drawPolyline(int[] xPoints, int[] yPoints, + int nPoints) { + if (nPoints > 0) { + GeneralPath path = new GeneralPath(); + path.moveTo(xPoints[0], yPoints[0]); + for (int i = 1; i < nPoints; i++) { + path.lineTo(xPoints[i], yPoints[i]); + } + + draw(path); + } + } + + /** + * Draws a closed polygon defined by + * arrays of x and y coordinates. + * Each pair of (xy) coordinates defines a point. + *

    + * This method draws the polygon defined by nPoint line + * segments, where the first nPoint - 1 + * line segments are line segments from + * (xPoints[i - 1], yPoints[i - 1]) + * to (xPoints[i], yPoints[i]), for + * 1 ≤ i ≤ nPoints. + * The figure is automatically closed by drawing a line connecting + * the final point to the first point, if those points are different. + * @param xPoints a an array of x coordinates. + * @param yPoints a an array of y coordinates. + * @param nPoints a the total number of points. + * @see java.awt.Graphics#fillPolygon(int[],int[],int) + * @see java.awt.Graphics#drawPolyline + */ + public void drawPolygon(int[] xPoints, int[] yPoints, + int nPoints) { + Polygon polygon = new Polygon(xPoints, yPoints, nPoints); + draw(polygon); + } + + + /** + * Fills a closed polygon defined by + * arrays of x and y coordinates. + *

    + * This method draws the polygon defined by nPoint line + * segments, where the first nPoint - 1 + * line segments are line segments from + * (xPoints[i - 1], yPoints[i - 1]) + * to (xPoints[i], yPoints[i]), for + * 1 ≤ i ≤ nPoints. + * The figure is automatically closed by drawing a line connecting + * the final point to the first point, if those points are different. + *

    + * The area inside the polygon is defined using an + * even-odd fill rule, also known as the alternating rule. + * @param xPoints a an array of x coordinates. + * @param yPoints a an array of y coordinates. + * @param nPoints a the total number of points. + * @see java.awt.Graphics#drawPolygon(int[], int[], int) + */ + public void fillPolygon(int[] xPoints, int[] yPoints, + int nPoints) { + Polygon polygon = new Polygon(xPoints, yPoints, nPoints); + fill(polygon); + } + + /** + * Draws the text given by the specified string, using this + * graphics context's current font and color. The baseline of the + * first character is at position (xy) in this + * graphics context's coordinate system. + * @param str the string to be drawn. + * @param x the x coordinate. + * @param y the y coordinate. + * @see java.awt.Graphics#drawBytes + * @see java.awt.Graphics#drawChars + */ + public void drawString(String str, int x, int y) { + drawString(str, (float)x, (float)y); + } + + /** + * Generic implementation for drawing attributed strings using TextLayout. + * + * @param iterator the iterator whose text is to be rendered + * @param x the x coordinate where the iterator's text is to be rendered + * @param y the y coordinate where the iterator's text is to be rendered + * @see java.awt.Graphics2D#drawString (java.text.AttributedCharacterIterator, + * float, float) + */ + public void drawString(AttributedCharacterIterator iterator, float x, float y) { + if (inPossibleRecursion) { + System.err.println("Called itself: drawString(AttributedCharacterIterator)"); + } else { + inPossibleRecursion = true; + TextLayout layout = new TextLayout(iterator, getFontRenderContext()); + layout.draw(this, x, y); + inPossibleRecursion = false; + } + } + + /** + * Draws the text given by the specified iterator, using this + * graphics context's current color. The iterator has to specify a font + * for each character. The baseline of the + * first character is at position (xy) in this + * graphics context's coordinate system. + * @param iterator the iterator whose text is to be drawn + * @param x the x coordinate. + * @param y the y coordinate. + * @see java.awt.Graphics#drawBytes + * @see java.awt.Graphics#drawChars + */ + public void drawString(AttributedCharacterIterator iterator, + int x, int y) { + drawString(iterator, (float)x, (float)y); + } + + /** + * Draws as much of the specified image as is currently available. + * The image is drawn with its top-left corner at + * (xy) in this graphics context's coordinate + * space. Transparent pixels are drawn in the specified + * background color. + *

    + * This operation is equivalent to filling a rectangle of the + * width and height of the specified image with the given color and then + * drawing the image on top of it, but possibly more efficient. + *

    + * This method returns immediately in all cases, even if the + * complete image has not yet been loaded, and it has not been dithered + * and converted for the current output device. + *

    + * If the image has not yet been completely loaded, then + * drawImage returns false. As more of + * the image becomes available, the process that draws the image notifies + * the specified image observer. + * @param img the specified image to be drawn. + * @param x the x coordinate. + * @param y the y coordinate. + * @param bgcolor the background color to paint under the + * non-opaque portions of the image. + * @param observer object to be notified as more of + * the image is converted. + * @see java.awt.Image + * @see java.awt.image.ImageObserver + * @see java.awt.image.ImageObserver#imageUpdate(java.awt.Image, int, int, int, int, int) + */ + public boolean drawImage(Image img, int x, int y, + Color bgcolor, + ImageObserver observer) { + return drawImage(img, x, y, img.getWidth(null), img.getHeight(null), + bgcolor, observer); + } + + + /** + * Draws as much of the specified image as has already been scaled + * to fit inside the specified rectangle. + *

    + * The image is drawn inside the specified rectangle of this + * graphics context's coordinate space, and is scaled if + * necessary. Transparent pixels are drawn in the specified + * background color. + * This operation is equivalent to filling a rectangle of the + * width and height of the specified image with the given color and then + * drawing the image on top of it, but possibly more efficient. + *

    + * This method returns immediately in all cases, even if the + * entire image has not yet been scaled, dithered, and converted + * for the current output device. + * If the current output representation is not yet complete then + * drawImage returns false. As more of + * the image becomes available, the process that draws the image notifies + * the specified image observer. + *

    + * A scaled version of an image will not necessarily be + * available immediately just because an unscaled version of the + * image has been constructed for this output device. Each size of + * the image may be cached separately and generated from the original + * data in a separate image production sequence. + * @param img the specified image to be drawn. + * @param x the x coordinate. + * @param y the y coordinate. + * @param width the width of the rectangle. + * @param height the height of the rectangle. + * @param bgcolor the background color to paint under the + * non-opaque portions of the image. + * @param observer object to be notified as more of + * the image is converted. + * @see java.awt.Image + * @see java.awt.image.ImageObserver + * @see java.awt.image.ImageObserver#imageUpdate(java.awt.Image, int, int, int, int, int) + */ + public boolean drawImage(Image img, int x, int y, + int width, int height, + Color bgcolor, + ImageObserver observer) { + Paint paint = gc.getPaint(); + gc.setPaint(bgcolor); + fillRect(x, y, width, height); + gc.setPaint(paint); + drawImage(img, x, y, width, height, observer); + + return true; + } + + /** + * Draws as much of the specified area of the specified image as is + * currently available, scaling it on the fly to fit inside the + * specified area of the destination drawable surface. Transparent pixels + * do not affect whatever pixels are already there. + *

    + * This method returns immediately in all cases, even if the + * image area to be drawn has not yet been scaled, dithered, and converted + * for the current output device. + * If the current output representation is not yet complete then + * drawImage returns false. As more of + * the image becomes available, the process that draws the image notifies + * the specified image observer. + *

    + * This method always uses the unscaled version of the image + * to render the scaled rectangle and performs the required + * scaling on the fly. It does not use a cached, scaled version + * of the image for this operation. Scaling of the image from source + * to destination is performed such that the first coordinate + * of the source rectangle is mapped to the first coordinate of + * the destination rectangle, and the second source coordinate is + * mapped to the second destination coordinate. The subimage is + * scaled and flipped as needed to preserve those mappings. + * @param img the specified image to be drawn + * @param dx1 the x coordinate of the first corner of the + * destination rectangle. + * @param dy1 the y coordinate of the first corner of the + * destination rectangle. + * @param dx2 the x coordinate of the second corner of the + * destination rectangle. + * @param dy2 the y coordinate of the second corner of the + * destination rectangle. + * @param sx1 the x coordinate of the first corner of the + * source rectangle. + * @param sy1 the y coordinate of the first corner of the + * source rectangle. + * @param sx2 the x coordinate of the second corner of the + * source rectangle. + * @param sy2 the y coordinate of the second corner of the + * source rectangle. + * @param observer object to be notified as more of the image is + * scaled and converted. + * @see java.awt.Image + * @see java.awt.image.ImageObserver + * @see java.awt.image.ImageObserver#imageUpdate(java.awt.Image, int, int, int, int, int) + * @since JDK1.1 + */ + public boolean drawImage(Image img, + int dx1, int dy1, int dx2, int dy2, + int sx1, int sy1, int sx2, int sy2, + ImageObserver observer) { + BufferedImage src = new BufferedImage(img.getWidth(null), img.getHeight(null), BufferedImage.TYPE_INT_ARGB); + Graphics2D g = src.createGraphics(); + g.drawImage(img, 0, 0, null); + g.dispose(); + + src = src.getSubimage(sx1, sy1, sx2 - sx1, sy2 - sy1); + + return drawImage(src, dx1, dy1, dx2 - dx1, dy2 - dy1, observer); + } + + + /** + * Draws as much of the specified area of the specified image as is + * currently available, scaling it on the fly to fit inside the + * specified area of the destination drawable surface. + *

    + * Transparent pixels are drawn in the specified background color. + * This operation is equivalent to filling a rectangle of the + * width and height of the specified image with the given color and then + * drawing the image on top of it, but possibly more efficient. + *

    + * This method returns immediately in all cases, even if the + * image area to be drawn has not yet been scaled, dithered, and converted + * for the current output device. + * If the current output representation is not yet complete then + * drawImage returns false. As more of + * the image becomes available, the process that draws the image notifies + * the specified image observer. + *

    + * This method always uses the unscaled version of the image + * to render the scaled rectangle and performs the required + * scaling on the fly. It does not use a cached, scaled version + * of the image for this operation. Scaling of the image from source + * to destination is performed such that the first coordinate + * of the source rectangle is mapped to the first coordinate of + * the destination rectangle, and the second source coordinate is + * mapped to the second destination coordinate. The subimage is + * scaled and flipped as needed to preserve those mappings. + * @param img the specified image to be drawn + * @param dx1 the x coordinate of the first corner of the + * destination rectangle. + * @param dy1 the y coordinate of the first corner of the + * destination rectangle. + * @param dx2 the x coordinate of the second corner of the + * destination rectangle. + * @param dy2 the y coordinate of the second corner of the + * destination rectangle. + * @param sx1 the x coordinate of the first corner of the + * source rectangle. + * @param sy1 the y coordinate of the first corner of the + * source rectangle. + * @param sx2 the x coordinate of the second corner of the + * source rectangle. + * @param sy2 the y coordinate of the second corner of the + * source rectangle. + * @param bgcolor the background color to paint under the + * non-opaque portions of the image. + * @param observer object to be notified as more of the image is + * scaled and converted. + * @see java.awt.Image + * @see java.awt.image.ImageObserver + * @see java.awt.image.ImageObserver#imageUpdate(java.awt.Image, int, int, int, int, int) + * @since JDK1.1 + */ + public boolean drawImage(Image img, + int dx1, int dy1, int dx2, int dy2, + int sx1, int sy1, int sx2, int sy2, + Color bgcolor, + ImageObserver observer) { + Paint paint = gc.getPaint(); + gc.setPaint(bgcolor); + fillRect(dx1, dy1, dx2 - dx1, dy2 - dy1); + gc.setPaint(paint); + return drawImage(img, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, observer); + } + + + /** + * Renders an image, applying a transform from image space into user space + * before drawing. + * The transformation from user space into device space is done with + * the current Transform in the Graphics2D. + * The specified transformation is applied to the image before the + * transform attribute in the Graphics2D context is applied. + * The rendering attributes applied include the Clip, + * Transform, and Composite attributes. + * Note that no rendering is done if the specified transform is + * noninvertible. + * @param img the Image to be rendered + * @param xform the transformation from image space into user space + * @param obs the {@link ImageObserver} + * to be notified as more of the Image + * is converted + * @return true if the Image is + * fully loaded and completely rendered; + * false if the Image is still being loaded. + * @see #transform + * @see #setTransform + * @see #setComposite + * @see #clip + * @see #setClip(Shape) + */ + public boolean drawImage(Image img, + AffineTransform xform, + ImageObserver obs) { + boolean retVal = true; + if (xform == null) { + xform = new AffineTransform(); + } + if (xform.getDeterminant() != 0) { + AffineTransform inverseTransform = null; + try { + inverseTransform = xform.createInverse(); + } catch (NoninvertibleTransformException e) { + // Should never happen since we checked the + // matrix determinant + throw new RuntimeException(e); + } + + gc.transform(xform); + retVal = drawImage(img, 0, 0, null); + gc.transform(inverseTransform); + } else { + AffineTransform savTransform = new AffineTransform(gc.getTransform()); + gc.transform(xform); + retVal = drawImage(img, 0, 0, null); + gc.setTransform(savTransform); + } + + return retVal; + + } + + + /** + * Renders a BufferedImage that is + * filtered with a + * {@link BufferedImageOp}. + * The rendering attributes applied include the Clip, + * Transform + * and Composite attributes. This is equivalent to: + *

    +     * img1 = op.filter(img, null);
    +     * drawImage(img1, new AffineTransform(1f,0f,0f,1f,x,y), null);
    +     * 
    + * @param img the BufferedImage to be rendered + * @param op the filter to be applied to the image before rendering + * @param x the x coordinate in user space where the image is rendered + * @param y the y coordinate in user space where the image is rendered + * @see #transform + * @see #setTransform + * @see #setComposite + * @see #clip + * @see #setClip(Shape) + */ + public void drawImage(BufferedImage img, + BufferedImageOp op, + int x, + int y) { + img = op.filter(img, null); + drawImage(img, x, y, null); + } + + + + /** + * Renders the text of the specified + * {@link GlyphVector} using + * the Graphics2D context's rendering attributes. + * The rendering attributes applied include the Clip, + * Transform, Paint, and + * Composite attributes. The GlyphVector + * specifies individual glyphs from a {@link Font}. + * The GlyphVector can also contain the glyph positions. + * This is the fastest way to render a set of characters to the + * screen. + * + * @param g the GlyphVector to be rendered + * @param x the x position in user space where the glyphs should be + * rendered + * @param y the y position in user space where the glyphs should be + * rendered + * + * @see java.awt.Font#createGlyphVector(FontRenderContext, char[]) + * @see java.awt.font.GlyphVector + * @see #setPaint + * @see java.awt.Graphics#setColor + * @see #setTransform + * @see #setComposite + * @see #setClip(Shape) + */ + public void drawGlyphVector(GlyphVector g, float x, float y) { + Shape glyphOutline = g.getOutline(x, y); + fill(glyphOutline); + } + + /** + * Checks whether or not the specified Shape intersects + * the specified {@link Rectangle}, which is in device + * space. If onStroke is false, this method checks + * whether or not the interior of the specified Shape + * intersects the specified Rectangle. If + * onStroke is true, this method checks + * whether or not the Stroke of the specified + * Shape outline intersects the specified + * Rectangle. + * The rendering attributes taken into account include the + * Clip, Transform, and Stroke + * attributes. + * @param rect the area in device space to check for a hit + * @param s the Shape to check for a hit + * @param onStroke flag used to choose between testing the + * stroked or the filled shape. If the flag is true, the + * Stroke oultine is tested. If the flag is + * false, the filled Shape is tested. + * @return true if there is a hit; false + * otherwise. + * @see #setStroke + * @see #fill(Shape) + * @see #draw(Shape) + * @see #transform + * @see #setTransform + * @see #clip + * @see #setClip(Shape) + */ + public boolean hit(Rectangle rect, + Shape s, + boolean onStroke) { + if (onStroke) { + s = gc.getStroke().createStrokedShape(s); + } + + s = gc.getTransform().createTransformedShape(s); + + return s.intersects(rect); + } + + /** + * Sets the Composite for the Graphics2D context. + * The Composite is used in all drawing methods such as + * drawImage, drawString, draw, + * and fill. It specifies how new pixels are to be combined + * with the existing pixels on the graphics device during the rendering + * process. + *

    If this Graphics2D context is drawing to a + * Component on the display screen and the + * Composite is a custom object rather than an + * instance of the AlphaComposite class, and if + * there is a security manager, its checkPermission + * method is called with an AWTPermission("readDisplayPixels") + * permission. + * @param comp the Composite object to be used for rendering + * @throws SecurityException + * if a custom Composite object is being + * used to render to the screen and a security manager + * is set and its checkPermission method + * does not allow the operation. + * @see java.awt.Graphics#setXORMode + * @see java.awt.Graphics#setPaintMode + * @see java.awt.AlphaComposite + */ + public void setComposite(Composite comp) { + gc.setComposite(comp); + } + + + /** + * Sets the Paint attribute for the + * Graphics2D context. Calling this method + * with a null Paint object does + * not have any effect on the current Paint attribute + * of this Graphics2D. + * @param paint the Paint object to be used to generate + * color during the rendering process, or null + * @see java.awt.Graphics#setColor + */ + public void setPaint(Paint paint) { + gc.setPaint(paint); + } + + + /** + * Sets the Stroke for the Graphics2D context. + * @param s the Stroke object to be used to stroke a + * Shape during the rendering process + */ + public void setStroke(Stroke s) { + gc.setStroke(s); + } + + + /** + * Sets the value of a single preference for the rendering algorithms. + * Hint categories include controls for rendering quality and overall + * time/quality trade-off in the rendering process. Refer to the + * RenderingHints class for definitions of some common + * keys and values. + * @param hintKey the key of the hint to be set. + * @param hintValue the value indicating preferences for the specified + * hint category. + * @see RenderingHints + */ + public void setRenderingHint(RenderingHints.Key hintKey, Object hintValue) { + gc.setRenderingHint(hintKey, hintValue); + } + + + /** + * Returns the value of a single preference for the rendering algorithms. + * Hint categories include controls for rendering quality and overall + * time/quality trade-off in the rendering process. Refer to the + * RenderingHints class for definitions of some common + * keys and values. + * @param hintKey the key corresponding to the hint to get. + * @return an object representing the value for the specified hint key. + * Some of the keys and their associated values are defined in the + * RenderingHints class. + * @see RenderingHints + */ + public Object getRenderingHint(RenderingHints.Key hintKey) { + return gc.getRenderingHint(hintKey); + } + + + /** + * Replaces the values of all preferences for the rendering + * algorithms with the specified hints. + * The existing values for all rendering hints are discarded and + * the new set of known hints and values are initialized from the + * specified {@link Map} object. + * Hint categories include controls for rendering quality and + * overall time/quality trade-off in the rendering process. + * Refer to the RenderingHints class for definitions of + * some common keys and values. + * @param hints the rendering hints to be set + * @see RenderingHints + */ + public void setRenderingHints(Map hints) { + gc.setRenderingHints(hints); + } + + + /** + * Sets the values of an arbitrary number of preferences for the + * rendering algorithms. + * Only values for the rendering hints that are present in the + * specified Map object are modified. + * All other preferences not present in the specified + * object are left unmodified. + * Hint categories include controls for rendering quality and + * overall time/quality trade-off in the rendering process. + * Refer to the RenderingHints class for definitions of + * some common keys and values. + * @param hints the rendering hints to be set + * @see RenderingHints + */ + public void addRenderingHints(Map hints) { + gc.addRenderingHints(hints); + } + + + /** + * Gets the preferences for the rendering algorithms. Hint categories + * include controls for rendering quality and overall time/quality + * trade-off in the rendering process. + * Returns all of the hint key/value pairs that were ever specified in + * one operation. Refer to the + * RenderingHints class for definitions of some common + * keys and values. + * @return a reference to an instance of RenderingHints + * that contains the current preferences. + * @see RenderingHints + */ + public RenderingHints getRenderingHints() { + return gc.getRenderingHints(); + } + + /** + * Concatenates the current + * Graphics2D Transform + * with a translation transform. + * Subsequent rendering is translated by the specified + * distance relative to the previous position. + * This is equivalent to calling transform(T), where T is an + * AffineTransform represented by the following matrix: + *

    +     *          [   1    0    tx  ]
    +     *          [   0    1    ty  ]
    +     *          [   0    0    1   ]
    +     * 
    + * @param tx the distance to translate along the x-axis + * @param ty the distance to translate along the y-axis + */ + public void translate(double tx, double ty) { + gc.translate(tx, ty); + } + + + /** + * Concatenates the current Graphics2D + * Transform with a rotation transform. + * Subsequent rendering is rotated by the specified radians relative + * to the previous origin. + * This is equivalent to calling transform(R), where R is an + * AffineTransform represented by the following matrix: + *
    +     *          [   cos(theta)    -sin(theta)    0   ]
    +     *          [   sin(theta)     cos(theta)    0   ]
    +     *          [       0              0         1   ]
    +     * 
    + * Rotating with a positive angle theta rotates points on the positive + * x axis toward the positive y axis. + * @param theta the angle of rotation in radians + */ + public void rotate(double theta) { + gc.rotate(theta); + } + + + /** + * Concatenates the current Graphics2D + * Transform with a translated rotation + * transform. Subsequent rendering is transformed by a transform + * which is constructed by translating to the specified location, + * rotating by the specified radians, and translating back by the same + * amount as the original translation. This is equivalent to the + * following sequence of calls: + *
    +     *          translate(x, y);
    +     *          rotate(theta);
    +     *          translate(-x, -y);
    +     * 
    + * Rotating with a positive angle theta rotates points on the positive + * x axis toward the positive y axis. + * @param theta the angle of rotation in radians + * @param x the x coordinate of the origin of the rotation + * @param y the y coordinate of the origin of the rotation + */ + public void rotate(double theta, double x, double y) { + gc.rotate(theta, x, y); + } + + + /** + * Concatenates the current Graphics2D + * Transform with a scaling transformation + * Subsequent rendering is resized according to the specified scaling + * factors relative to the previous scaling. + * This is equivalent to calling transform(S), where S is an + * AffineTransform represented by the following matrix: + *
    +     *          [   sx   0    0   ]
    +     *          [   0    sy   0   ]
    +     *          [   0    0    1   ]
    +     * 
    + * @param sx the amount by which X coordinates in subsequent + * rendering operations are multiplied relative to previous + * rendering operations. + * @param sy the amount by which Y coordinates in subsequent + * rendering operations are multiplied relative to previous + * rendering operations. + */ + public void scale(double sx, double sy) { + gc.scale(sx, sy); + } + + /** + * Concatenates the current Graphics2D + * Transform with a shearing transform. + * Subsequent renderings are sheared by the specified + * multiplier relative to the previous position. + * This is equivalent to calling transform(SH), where SH + * is an AffineTransform represented by the following + * matrix: + *
    +     *          [   1   shx   0   ]
    +     *          [  shy   1    0   ]
    +     *          [   0    0    1   ]
    +     * 
    + * @param shx the multiplier by which coordinates are shifted in + * the positive X axis direction as a function of their Y coordinate + * @param shy the multiplier by which coordinates are shifted in + * the positive Y axis direction as a function of their X coordinate + */ + public void shear(double shx, double shy) { + gc.shear(shx, shy); + } + + /** + * Composes an AffineTransform object with the + * Transform in this Graphics2D according + * to the rule last-specified-first-applied. If the current + * Transform is Cx, the result of composition + * with Tx is a new Transform Cx'. Cx' becomes the + * current Transform for this Graphics2D. + * Transforming a point p by the updated Transform Cx' is + * equivalent to first transforming p by Tx and then transforming + * the result by the original Transform Cx. In other + * words, Cx'(p) = Cx(Tx(p)). A copy of the Tx is made, if necessary, + * so further modifications to Tx do not affect rendering. + * @param tx the AffineTransform object to be composed with + * the current Transform + * @see #setTransform + * @see AffineTransform + */ + public void transform(AffineTransform tx) { + gc.transform(tx); + } + + /** + * Sets the Transform in the Graphics2D + * context. + * @param tx the AffineTransform object to be used in the + * rendering process + * @see #transform + * @see AffineTransform + */ + public void setTransform(AffineTransform tx) { + gc.setTransform(tx); + } + + + /** + * Returns a copy of the current Transform in the + * Graphics2D context. + * @return the current AffineTransform in the + * Graphics2D context. + * @see #transform + * @see #setTransform + */ + public AffineTransform getTransform() { + return gc.getTransform(); + } + + + /** + * Returns the current Paint of the + * Graphics2D context. + * @return the current Graphics2D Paint, + * which defines a color or pattern. + * @see #setPaint + * @see java.awt.Graphics#setColor + */ + public Paint getPaint() { + return gc.getPaint(); + } + + + /** + * Returns the current Composite in the + * Graphics2D context. + * @return the current Graphics2D Composite, + * which defines a compositing style. + * @see #setComposite + */ + public Composite getComposite() { + return gc.getComposite(); + } + + + /** + * Sets the background color for the Graphics2D context. + * The background color is used for clearing a region. + * When a Graphics2D is constructed for a + * Component, the background color is + * inherited from the Component. Setting the background color + * in the Graphics2D context only affects the subsequent + * clearRect calls and not the background color of the + * Component. To change the background + * of the Component, use appropriate methods of + * the Component. + * @param color the background color that isused in + * subsequent calls to clearRect + * @see #getBackground + * @see java.awt.Graphics#clearRect + */ + public void setBackground(Color color) { + gc.setBackground(color); + } + + + /** + * Returns the background color used for clearing a region. + * @return the current Graphics2D Color, + * which defines the background color. + * @see #setBackground + */ + public Color getBackground() { + return gc.getBackground(); + } + + + /** + * Returns the current Stroke in the + * Graphics2D context. + * @return the current Graphics2D Stroke, + * which defines the line style. + * @see #setStroke + */ + public Stroke getStroke() { + return gc.getStroke(); + } + + + /** + * Intersects the current Clip with the interior of the + * specified Shape and sets the Clip to the + * resulting intersection. The specified Shape is + * transformed with the current Graphics2D + * Transform before being intersected with the current + * Clip. This method is used to make the current + * Clip smaller. + * To make the Clip larger, use setClip. + * The user clip modified by this method is independent of the + * clipping associated with device bounds and visibility. If no clip has + * previously been set, or if the clip has been cleared using + * {@link java.awt.Graphics#setClip(Shape) setClip} with a + * null argument, the specified Shape becomes + * the new user clip. + * @param s the Shape to be intersected with the current + * Clip. If s is null, + * this method clears the current Clip. + */ + public void clip(Shape s) { + gc.clip(s); + } + + + /** + * Get the rendering context of the Font within this + * Graphics2D context. + * The {@link FontRenderContext} + * encapsulates application hints such as anti-aliasing and + * fractional metrics, as well as target device specific information + * such as dots-per-inch. This information should be provided by the + * application when using objects that perform typographical + * formatting, such as Font and + * TextLayout. This information should also be provided + * by applications that perform their own layout and need accurate + * measurements of various characteristics of glyphs such as advance + * and line height when various rendering hints have been applied to + * the text rendering. + * + * @return a reference to an instance of FontRenderContext. + * @see java.awt.font.FontRenderContext + * @see java.awt.Font#createGlyphVector(FontRenderContext,char[]) + * @see java.awt.font.TextLayout + * @since JDK1.2 + */ + public FontRenderContext getFontRenderContext() { + return gc.getFontRenderContext(); + } + + /** + * @return the {@link GraphicContext} of this Graphics2D. + */ + public GraphicContext getGraphicContext() { + return gc; + } +} diff --git a/src/main/java/org/apache/xmlgraphics/java2d/AbstractGraphicsConfiguration.java b/src/main/java/org/apache/xmlgraphics/java2d/AbstractGraphicsConfiguration.java new file mode 100644 index 0000000..cc208c0 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/java2d/AbstractGraphicsConfiguration.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: AbstractGraphicsConfiguration.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.java2d; + +import java.awt.image.VolatileImage; + +/** + * Adapter to allow subclassing java.awt.GraphicsConfiguration without + * compilation errors. + * The version for JDK 1.4 needs to add an override for the abstract + * createCompatibleVolatileImage() method. It can't be overidden + * for JDK 1.3 because there is no VolatileImage there. + * + */ +public abstract class AbstractGraphicsConfiguration extends java.awt.GraphicsConfiguration { + + /** + * @see java.awt.GraphicsConfiguration#createCompatibleVolatileImage(int, int) + * @since JDK 1.4 + */ + public VolatileImage createCompatibleVolatileImage(int width, int height) { + return null; + } + + /** + * @see java.awt.GraphicsConfiguration#createCompatibleVolatileImage(int, int, int) + * @since JDK 1.5 + */ + public VolatileImage createCompatibleVolatileImage(int width, int height, int transparency) { + return null; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/java2d/CMYKColorSpace.java b/src/main/java/org/apache/xmlgraphics/java2d/CMYKColorSpace.java new file mode 100644 index 0000000..7f7c433 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/java2d/CMYKColorSpace.java @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: CMYKColorSpace.java 954487 2010-06-14 14:40:26Z jeremias $ */ + +package org.apache.xmlgraphics.java2d; + +import java.awt.color.ColorSpace; + +/** + * This class represents an uncalibrated CMYK color space. + * @deprecated This class has been moved to the "color" subpackage. + */ +public class CMYKColorSpace extends ColorSpace { + + private static final long serialVersionUID = 2925508946083542974L; + + private static CMYKColorSpace instance; + + /** + * @see java.awt.color.ColorSpace#ColorSpace(int, int) + */ + protected CMYKColorSpace(int type, int numcomponents) { + super(type, numcomponents); + } + + /** + * Returns an instance of an uncalibrated CMYK color space. + * @return CMYKColorSpace the requested color space object + */ + public static CMYKColorSpace getInstance() { + if (instance == null) { + instance = new CMYKColorSpace(TYPE_CMYK, 4); + } + return instance; + } + + /** {@inheritDoc} */ + public float[] toRGB(float[] colorvalue) { + return new float [] { + (1 - colorvalue[0]) * (1 - colorvalue[3]), + (1 - colorvalue[1]) * (1 - colorvalue[3]), + (1 - colorvalue[2]) * (1 - colorvalue[3])}; + } + + /** {@inheritDoc} */ + public float[] fromRGB(float[] rgbvalue) { + throw new UnsupportedOperationException("NYI"); + } + + /** {@inheritDoc} */ + public float[] toCIEXYZ(float[] colorvalue) { + throw new UnsupportedOperationException("NYI"); + } + + /** {@inheritDoc} */ + public float[] fromCIEXYZ(float[] colorvalue) { + throw new UnsupportedOperationException("NYI"); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/java2d/DefaultGraphics2D.java b/src/main/java/org/apache/xmlgraphics/java2d/DefaultGraphics2D.java new file mode 100644 index 0000000..a99d8ea --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/java2d/DefaultGraphics2D.java @@ -0,0 +1,403 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DefaultGraphics2D.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.java2d; + +import java.awt.Color; +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.GraphicsConfiguration; +import java.awt.Image; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.image.BufferedImage; +import java.awt.image.ImageObserver; +import java.awt.image.RenderedImage; +import java.awt.image.renderable.RenderableImage; +import java.text.AttributedCharacterIterator; + +// CSOFF: WhitespaceAround + +/** + * This concrete implementation of AbstractGraphics2D is a + * simple help to programmers to get started with their own + * implementation of Graphics2D. + * DefaultGraphics2D implements all the abstract methods + * is AbstractGraphics2D and makes it easy to start + * implementing a Graphic2D piece-meal. + * + * @version $Id: DefaultGraphics2D.java 1732018 2016-02-24 04:51:06Z gadams $ + * @see org.apache.xmlgraphics.java2d.AbstractGraphics2D + * + * Originally authored by Vincent Hardy. + */ +public class DefaultGraphics2D extends AbstractGraphics2D { + /** + * Default constructor + */ + public DefaultGraphics2D(boolean textAsShapes) { + super(textAsShapes); + } + + /** + * This constructor supports the create method + */ + public DefaultGraphics2D(DefaultGraphics2D g) { + super(g); + } + + /** + * Creates a new Graphics object that is + * a copy of this Graphics object. + * @return a new graphics context that is a copy of + * this graphics context. + */ + public Graphics create() { + return new DefaultGraphics2D(this); + } + + /** + * Draws as much of the specified image as is currently available. + * The image is drawn with its top-left corner at + * (xy) in this graphics context's coordinate + * space. Transparent pixels in the image do not affect whatever + * pixels are already there. + *

    + * This method returns immediately in all cases, even if the + * complete image has not yet been loaded, and it has not been dithered + * and converted for the current output device. + *

    + * If the image has not yet been completely loaded, then + * drawImage returns false. As more of + * the image becomes available, the process that draws the image notifies + * the specified image observer. + * @param img the specified image to be drawn. + * @param x the x coordinate. + * @param y the y coordinate. + * @param observer object to be notified as more of + * the image is converted. + * @see java.awt.Image + * @see java.awt.image.ImageObserver + * @see java.awt.image.ImageObserver#imageUpdate(java.awt.Image, int, int, int, int, int) + */ + public boolean drawImage(Image img, int x, int y, ImageObserver observer) { + System.err.println("drawImage"); + return true; + } + + /** + * Draws as much of the specified image as has already been scaled + * to fit inside the specified rectangle. + *

    + * The image is drawn inside the specified rectangle of this + * graphics context's coordinate space, and is scaled if + * necessary. Transparent pixels do not affect whatever pixels + * are already there. + *

    + * This method returns immediately in all cases, even if the + * entire image has not yet been scaled, dithered, and converted + * for the current output device. + * If the current output representation is not yet complete, then + * drawImage returns false. As more of + * the image becomes available, the process that draws the image notifies + * the image observer by calling its imageUpdate method. + *

    + * A scaled version of an image will not necessarily be + * available immediately just because an unscaled version of the + * image has been constructed for this output device. Each size of + * the image may be cached separately and generated from the original + * data in a separate image production sequence. + * @param img the specified image to be drawn. + * @param x the x coordinate. + * @param y the y coordinate. + * @param width the width of the rectangle. + * @param height the height of the rectangle. + * @param observer object to be notified as more of + * the image is converted. + * @see java.awt.Image + * @see java.awt.image.ImageObserver + * @see java.awt.image.ImageObserver#imageUpdate(java.awt.Image, int, int, int, int, int) + */ + public boolean drawImage(Image img, int x, int y, + int width, int height, + ImageObserver observer) { + System.out.println("drawImage"); + return true; + } + + /** + * Disposes of this graphics context and releases + * any system resources that it is using. + * A Graphics object cannot be used after + * disposehas been called. + *

    + * When a Java program runs, a large number of Graphics + * objects can be created within a short time frame. + * Although the finalization process of the garbage collector + * also disposes of the same system resources, it is preferable + * to manually free the associated resources by calling this + * method rather than to rely on a finalization process which + * may not run to completion for a long period of time. + *

    + * Graphics objects which are provided as arguments to the + * paint and update methods + * of components are automatically released by the system when + * those methods return. For efficiency, programmers should + * call dispose when finished using + * a Graphics object only if it was created + * directly from a component or another Graphics object. + * @see java.awt.Graphics#finalize + * @see java.awt.Component#paint + * @see java.awt.Component#update + * @see java.awt.Component#getGraphics + * @see java.awt.Graphics#create() + */ + public void dispose() { + System.out.println("dispose"); + } + + /** + * Strokes the outline of a Shape using the settings of the + * current Graphics2D context. The rendering attributes + * applied include the Clip, Transform, + * Paint, Composite and + * Stroke attributes. + * @param s the Shape to be rendered + * @see #setStroke(java.awt.Stroke) + * @see #setPaint(java.awt.Paint) + * @see java.awt.Graphics#setColor + * @see #setTransform(AffineTransform) + * @see #setClip(Shape) + * @see #setComposite(java.awt.Composite) + */ + public void draw(Shape s) { + System.out.println("draw(Shape)"); + } + + /** + * Renders a {@link RenderedImage}, + * applying a transform from image + * space into user space before drawing. + * The transformation from user space into device space is done with + * the current Transform in the Graphics2D. + * The specified transformation is applied to the image before the + * transform attribute in the Graphics2D context is applied. + * The rendering attributes applied include the Clip, + * Transform, and Composite attributes. Note + * that no rendering is done if the specified transform is + * noninvertible. + * @param img the image to be rendered + * @param xform the transformation from image space into user space + * @see #setTransform(AffineTransform) + * @see #setComposite(java.awt.Composite) + * @see #setClip(Shape) + */ + public void drawRenderedImage(RenderedImage img, + AffineTransform xform) { + System.out.println("drawRenderedImage"); + } + + + /** + * Renders a + * {@link RenderableImage}, + * applying a transform from image space into user space before drawing. + * The transformation from user space into device space is done with + * the current Transform in the Graphics2D. + * The specified transformation is applied to the image before the + * transform attribute in the Graphics2D context is applied. + * The rendering attributes applied include the Clip, + * Transform, and Composite attributes. Note + * that no rendering is done if the specified transform is + * noninvertible. + *

    + * Rendering hints set on the Graphics2D object might + * be used in rendering the RenderableImage. + * If explicit control is required over specific hints recognized by a + * specific RenderableImage, or if knowledge of which hints + * are used is required, then a RenderedImage should be + * obtained directly from the RenderableImage + * and rendered using + *{@link #drawRenderedImage(RenderedImage, AffineTransform) drawRenderedImage}. + * @param img the image to be rendered + * @param xform the transformation from image space into user space + * @see #setTransform(AffineTransform) + * @see #setComposite(java.awt.Composite) + * @see #setClip(Shape) + * @see #drawRenderedImage + */ + public void drawRenderableImage(RenderableImage img, + AffineTransform xform) { + System.out.println("drawRenderableImage"); + } + + /** + * Renders the text specified by the specified String, + * using the current Font and Paint attributes + * in the Graphics2D context. + * The baseline of the first character is at position + * (xy) in the User Space. + * The rendering attributes applied include the Clip, + * Transform, Paint, Font and + * Composite attributes. For characters in script systems + * such as Hebrew and Arabic, the glyphs can be rendered from right to + * left, in which case the coordinate supplied is the location of the + * leftmost character on the baseline. + * @param s the String to be rendered + * @param x the x coordinate where the String should be + * rendered + * @param y the y coordinate where the String should be + * rendered + * @see #setPaint(java.awt.Paint) + * @see java.awt.Graphics#setColor + * @see java.awt.Graphics#setFont + * @see #setTransform(AffineTransform) + * @see #setComposite(java.awt.Composite) + * @see #setClip(Shape) + */ + public void drawString(String s, float x, float y) { + System.out.println("drawString(String)"); + } + + /** + * Renders the text of the specified iterator, using the + * Graphics2D context's current Paint. The + * iterator must specify a font + * for each character. The baseline of the + * first character is at position (xy) in the + * User Space. + * The rendering attributes applied include the Clip, + * Transform, Paint, and + * Composite attributes. + * For characters in script systems such as Hebrew and Arabic, + * the glyphs can be rendered from right to left, in which case the + * coordinate supplied is the location of the leftmost character + * on the baseline. + * @param iterator the iterator whose text is to be rendered + * @param x the x coordinate where the iterator's text is to be rendered + * @param y the y coordinate where the iterator's text is to be rendered + * @see #setPaint(java.awt.Paint) + * @see java.awt.Graphics#setColor + * @see #setTransform(AffineTransform) + * @see #setComposite(java.awt.Composite) + * @see #setClip(Shape) + */ + public void drawString(AttributedCharacterIterator iterator, + float x, float y) { + System.err.println("drawString(AttributedCharacterIterator)"); + } + + + + /** + * Fills the interior of a Shape using the settings of the + * Graphics2D context. The rendering attributes applied + * include the Clip, Transform, + * Paint, and Composite. + * @param s the Shape to be filled + * @see #setPaint(java.awt.Paint) + * @see java.awt.Graphics#setColor + * @see #setTransform(AffineTransform) + * @see #setComposite(java.awt.Composite) + * @see #setClip(Shape) + */ + public void fill(Shape s) { + System.err.println("fill"); + } + + /** + * Returns the device configuration associated with this + * Graphics2D. + */ + public GraphicsConfiguration getDeviceConfiguration() { + System.out.println("getDeviceConviguration"); + return null; + } + + /** + * Used to create proper font metrics + */ + private Graphics2D fmg; + + { + BufferedImage bi + = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB); + + fmg = bi.createGraphics(); + } + + /** + * Gets the font metrics for the specified font. + * @return the font metrics for the specified font. + * @param f the specified font + * @see java.awt.Graphics#getFont + * @see java.awt.FontMetrics + * @see java.awt.Graphics#getFontMetrics() + */ + public FontMetrics getFontMetrics(Font f) { + return fmg.getFontMetrics(f); + } + + /** + * Sets the paint mode of this graphics context to alternate between + * this graphics context's current color and the new specified color. + * This specifies that logical pixel operations are performed in the + * XOR mode, which alternates pixels between the current color and + * a specified XOR color. + *

    + * When drawing operations are performed, pixels which are the + * current color are changed to the specified color, and vice versa. + *

    + * Pixels that are of colors other than those two colors are changed + * in an unpredictable but reversible manner; if the same figure is + * drawn twice, then all pixels are restored to their original values. + * @param c1 the XOR alternation color + */ + public void setXORMode(Color c1) { + System.out.println("setXORMode"); + } + + + /** + * Copies an area of the component by a distance specified by + * dx and dy. From the point specified + * by x and y, this method + * copies downwards and to the right. To copy an area of the + * component to the left or upwards, specify a negative value for + * dx or dy. + * If a portion of the source rectangle lies outside the bounds + * of the component, or is obscured by another window or component, + * copyArea will be unable to copy the associated + * pixels. The area that is omitted can be refreshed by calling + * the component's paint method. + * @param x the x coordinate of the source rectangle. + * @param y the y coordinate of the source rectangle. + * @param width the width of the source rectangle. + * @param height the height of the source rectangle. + * @param dx the horizontal distance to copy the pixels. + * @param dy the vertical distance to copy the pixels. + */ + public void copyArea(int x, int y, int width, int height, + int dx, int dy) { + System.out.println("copyArea"); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/java2d/Dimension2DDouble.java b/src/main/java/org/apache/xmlgraphics/java2d/Dimension2DDouble.java new file mode 100644 index 0000000..21f2569 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/java2d/Dimension2DDouble.java @@ -0,0 +1,98 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: Dimension2DDouble.java 1345683 2012-06-03 14:50:33Z gadams $ */ + +package org.apache.xmlgraphics.java2d; + +import java.awt.geom.Dimension2D; +import java.io.Serializable; + +/** + * Subclass of Dimension2D which takes double values. + */ +public class Dimension2DDouble extends Dimension2D implements Serializable { + + private static final long serialVersionUID = 7909028357685520189L; + + private double width; + private double height; + + /** + * Default constructor. + */ + public Dimension2DDouble() { + this.width = 0; + this.height = 0; + } + + /** + * Main constructor. + * @param width initial width + * @param height initial height + */ + public Dimension2DDouble(double width, double height) { + this.width = width; + this.height = height; + } + + /** {@inheritDoc} */ + public double getWidth() { + return width; + } + + /** {@inheritDoc} */ + public double getHeight() { + return height; + } + + /** {@inheritDoc} */ + public void setSize(double w, double h) { + this.width = w; + this.height = h; + } + + /** {@inheritDoc} */ + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof Dimension2DDouble) { + final Dimension2DDouble other = (Dimension2DDouble)obj; + if (Double.doubleToLongBits(height) != Double.doubleToLongBits(other.height)) { + return false; + } + if (Double.doubleToLongBits(width) != Double.doubleToLongBits(other.width)) { + return false; + } + return true; + } else { + return false; + } + } + + /** {@inheritDoc} */ + public int hashCode() { + double sum = width + height; + return (int)(sum * (sum + 1) / 2 + width); + } + + /** {@inheritDoc} */ + public String toString() { + return getClass().getName() + "[width=" + width + ",height=" + height + "]"; + } +} diff --git a/src/main/java/org/apache/xmlgraphics/java2d/GeneralGraphics2DImagePainter.java b/src/main/java/org/apache/xmlgraphics/java2d/GeneralGraphics2DImagePainter.java new file mode 100644 index 0000000..db1703e --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/java2d/GeneralGraphics2DImagePainter.java @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: GeneralGraphics2DImagePainter.java 1812121 2017-10-13 12:12:29Z ssteiner $ */ + +package org.apache.xmlgraphics.java2d; + +import java.awt.Graphics2D; + +import org.apache.xmlgraphics.ps.PSGenerator; + +public interface GeneralGraphics2DImagePainter extends Graphics2DImagePainter { + Graphics2D getGraphics(boolean textAsShapes, PSGenerator gen); + void addFallbackFont(String name, Object font); +} diff --git a/src/main/java/org/apache/xmlgraphics/java2d/GenericGraphicsDevice.java b/src/main/java/org/apache/xmlgraphics/java2d/GenericGraphicsDevice.java new file mode 100644 index 0000000..d805acf --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/java2d/GenericGraphicsDevice.java @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: GenericGraphicsDevice.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.java2d; + +import java.awt.GraphicsConfigTemplate; +import java.awt.GraphicsConfiguration; +import java.awt.GraphicsDevice; + +/** + * This implements the {@link GraphicsDevice} interface as appropriate for the various output + * configurations. + */ +public class GenericGraphicsDevice extends GraphicsDevice { + + /** + * The Graphics Config that created us... + */ + private final GraphicsConfiguration gc; + + /** + * Create a new graphics2D device. + * + * @param gc we should reference + */ + public GenericGraphicsDevice(GraphicsConfiguration gc) { + this.gc = gc; + } + + /** + * Ignore template and return the only config we have + * + * @param gct the template configuration + * @return the best configuration which is the only one + */ + public GraphicsConfiguration getBestConfiguration(GraphicsConfigTemplate gct) { + return gc; + } + + /** + * Return an array of our one GraphicsConfig + * + * @return an array containing the one graphics configuration + */ + public GraphicsConfiguration[] getConfigurations() { + return new GraphicsConfiguration[] {gc}; + } + + /** + * Return out sole GraphicsConfig. + * + * @return the graphics configuration that created this object + */ + public GraphicsConfiguration getDefaultConfiguration() { + return gc; + } + + /** + * Generate an IdString.. + * + * @return the ID string for this device, uses toString + */ + public String getIDstring() { + return toString(); + } + + /** + * Let the caller know that we are "a printer" + * + * @return the type which is always printer + */ + public int getType() { + return GraphicsDevice.TYPE_PRINTER; + } +} diff --git a/src/main/java/org/apache/xmlgraphics/java2d/GraphicContext.java b/src/main/java/org/apache/xmlgraphics/java2d/GraphicContext.java new file mode 100644 index 0000000..a80fe33 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/java2d/GraphicContext.java @@ -0,0 +1,925 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: GraphicContext.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.java2d; + +import java.awt.AlphaComposite; +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Composite; +import java.awt.Font; +import java.awt.Paint; +import java.awt.Rectangle; +import java.awt.RenderingHints; +import java.awt.Shape; +import java.awt.Stroke; +import java.awt.font.FontRenderContext; +import java.awt.geom.AffineTransform; +import java.awt.geom.Area; +import java.awt.geom.GeneralPath; +import java.awt.geom.NoninvertibleTransformException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +// CSOFF: OperatorWrap +// CSOFF: ParameterName +// CSOFF: WhitespaceAround + +/** + * Handles the attributes in a graphic context:
    + * + Composite
    + * + Font
    + * + Paint
    + * + Stroke
    + * + Clip
    + * + RenderingHints
    + * + AffineTransform
    + * + * @version $Id: GraphicContext.java 1732018 2016-02-24 04:51:06Z gadams $ + * + * Originally authored by Vincent Hardy and Christophe Jolif. + */ +public class GraphicContext implements Cloneable { + /** + * Default Transform to be used for creating FontRenderContext. + */ + protected AffineTransform defaultTransform = new AffineTransform(); + + /** + * Current AffineTransform. This is the concatenation + * of the original AffineTransform (i.e., last setTransform + * invocation) and the following transform invocations, + * as captured by originalTransform and the transformStack. + */ + protected AffineTransform transform = new AffineTransform(); + + /** + * Transform stack + */ + protected List transformStack = new ArrayList(); + + /** + * Defines whether the transform stack is valid or not. + * This is for use by the class clients. The client should + * validate the stack every time it starts using it. The + * stack becomes invalid when a new transform is set. + * @see #invalidateTransformStack() + * @see #isTransformStackValid + * @see #setTransform + */ + protected boolean transformStackValid = true; + + /** + * Current Paint + */ + protected Paint paint = Color.black; + + /** + * Current Stroke + */ + protected Stroke stroke = new BasicStroke(); + + /** + * Current Composite + */ + protected Composite composite = AlphaComposite.SrcOver; + + /** + * Current clip + */ + protected Shape clip; + + /** + * Current set of RenderingHints + */ + protected RenderingHints hints = new RenderingHints(null); + + /** + * Current Font + */ + protected Font font = new Font("sanserif", Font.PLAIN, 12); + + /** + * Current background color. + */ + protected Color background = new Color(0, 0, 0, 0); + + /** + * Current foreground color + */ + protected Color foreground = Color.black; + + /** + * Default constructor + */ + public GraphicContext() { + // to workaround a JDK bug + hints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_DEFAULT); + } + + /** + * @param defaultDeviceTransform Default affine transform applied to map the user space to the + * user space. + */ + public GraphicContext(AffineTransform defaultDeviceTransform) { + this(); + defaultTransform = new AffineTransform(defaultDeviceTransform); + transform = new AffineTransform(defaultTransform); + if (!defaultTransform.isIdentity()) { + transformStack.add(TransformStackElement.createGeneralTransformElement(defaultTransform)); + } + } + + /** + * Copy constructor. + * @param template the instance to make a copy of + */ + protected GraphicContext(GraphicContext template) { + this(template.defaultTransform); + // + // Now, copy each GC element in turn + // + + // Default transform + /* Set in constructor */ + + // Transform + this.transform = new AffineTransform(template.transform); + + // Transform stack + this.transformStack = new ArrayList(template.transformStack.size()); + for (int i = 0; i < template.transformStack.size(); i++) { + TransformStackElement stackElement + = (TransformStackElement)template.transformStack.get(i); + this.transformStack.add(stackElement.clone()); + } + + // Transform stack validity + this.transformStackValid = template.transformStackValid; + + // Paint (immutable by requirement) + this.paint = template.paint; + + // Stroke (immutable by requirement) + this.stroke = template.stroke; + + // Composite (immutable by requirement) + this.composite = template.composite; + + // Clip + if (template.clip != null) { + this.clip = new GeneralPath(template.clip); + } else { + this.clip = null; + } + + // RenderingHints + this.hints = (RenderingHints)template.hints.clone(); + + // Font (immutable) + this.font = template.font; + + // Background, Foreground (immutable) + this.background = template.background; + this.foreground = template.foreground; + } + + /** + * @return a deep copy of this context + */ + public Object clone() { + return new GraphicContext(this); + } + + /** + * Gets this graphics context's current color. + * @return this graphics context's current color. + * @see java.awt.Color + * @see java.awt.Graphics#setColor + */ + public Color getColor() { + return foreground; + } + + /** + * Sets this graphics context's current color to the specified + * color. All subsequent graphics operations using this graphics + * context use this specified color. + * @param c the new rendering color. + * @see java.awt.Color + * @see java.awt.Graphics#getColor + */ + public void setColor(Color c) { + if (c == null) { + return; + } + + if (paint != c) { + setPaint(c); + } + } + + /** + * Gets the current font. + * @return this graphics context's current font. + * @see java.awt.Font + * @see java.awt.Graphics#setFont + */ + public Font getFont() { + return font; + } + + /** + * Sets this graphics context's font to the specified font. + * All subsequent text operations using this graphics context + * use this font. + * @param font the font. + * @see java.awt.Graphics#getFont + */ + public void setFont(Font font) { + if (font != null) { + this.font = font; + } + } + + /** + * Returns the bounding rectangle of the current clipping area. + * This method refers to the user clip, which is independent of the + * clipping associated with device bounds and window visibility. + * If no clip has previously been set, or if the clip has been + * cleared using setClip(null), this method returns + * null. + * The coordinates in the rectangle are relative to the coordinate + * system origin of this graphics context. + * @return the bounding rectangle of the current clipping area, + * or null if no clip is set. + * @see java.awt.Graphics#getClip + * @see java.awt.Graphics#clipRect + * @see java.awt.Graphics#setClip(int, int, int, int) + * @see java.awt.Graphics#setClip(Shape) + * @since JDK1.1 + */ + public Rectangle getClipBounds() { + Shape c = getClip(); + if (c == null) { + return null; + } else { + return c.getBounds(); + } + } + + + /** + * Intersects the current clip with the specified rectangle. + * The resulting clipping area is the intersection of the current + * clipping area and the specified rectangle. If there is no + * current clipping area, either because the clip has never been + * set, or the clip has been cleared using setClip(null), + * the specified rectangle becomes the new clip. + * This method sets the user clip, which is independent of the + * clipping associated with device bounds and window visibility. + * This method can only be used to make the current clip smaller. + * To set the current clip larger, use any of the setClip methods. + * Rendering operations have no effect outside of the clipping area. + * @param x the x coordinate of the rectangle to intersect the clip with + * @param y the y coordinate of the rectangle to intersect the clip with + * @param width the width of the rectangle to intersect the clip with + * @param height the height of the rectangle to intersect the clip with + * @see #setClip(int, int, int, int) + * @see #setClip(Shape) + */ + public void clipRect(int x, int y, int width, int height) { + clip(new Rectangle(x, y, width, height)); + } + + + /** + * Sets the current clip to the rectangle specified by the given + * coordinates. This method sets the user clip, which is + * independent of the clipping associated with device bounds + * and window visibility. + * Rendering operations have no effect outside of the clipping area. + * @param x the x coordinate of the new clip rectangle. + * @param y the y coordinate of the new clip rectangle. + * @param width the width of the new clip rectangle. + * @param height the height of the new clip rectangle. + * @see java.awt.Graphics#clipRect + * @see java.awt.Graphics#setClip(Shape) + * @since JDK1.1 + */ + public void setClip(int x, int y, int width, int height) { + setClip(new Rectangle(x, y, width, height)); + } + + + /** + * Gets the current clipping area. + * This method returns the user clip, which is independent of the + * clipping associated with device bounds and window visibility. + * If no clip has previously been set, or if the clip has been + * cleared using setClip(null), this method returns + * null. + * @return a Shape object representing the + * current clipping area, or null if + * no clip is set. + * @see java.awt.Graphics#getClipBounds() + * @see java.awt.Graphics#clipRect + * @see java.awt.Graphics#setClip(int, int, int, int) + * @see java.awt.Graphics#setClip(Shape) + * @since JDK1.1 + */ + public Shape getClip() { + try { + return transform.createInverse().createTransformedShape(clip); + } catch (NoninvertibleTransformException e) { + return null; + } + } + + + /** + * Sets the current clipping area to an arbitrary clip shape. + * Not all objects that implement the Shape + * interface can be used to set the clip. The only + * Shape objects that are guaranteed to be + * supported are Shape objects that are + * obtained via the getClip method and via + * Rectangle objects. This method sets the + * user clip, which is independent of the clipping associated + * with device bounds and window visibility. + * @param clip the Shape to use to set the clip + * @see java.awt.Graphics#getClip() + * @see java.awt.Graphics#clipRect + * @see java.awt.Graphics#setClip(int, int, int, int) + * @since JDK1.1 + */ + public void setClip(Shape clip) { + if (clip != null) { + this.clip = transform.createTransformedShape(clip); + } else { + this.clip = null; + } + } + + /** + * Sets the Composite for the Graphics2D context. + * The Composite is used in all drawing methods such as + * drawImage, drawString, draw, + * and fill. It specifies how new pixels are to be combined + * with the existing pixels on the graphics device during the rendering + * process. + *

    If this Graphics2D context is drawing to a + * Component on the display screen and the + * Composite is a custom object rather than an + * instance of the AlphaComposite class, and if + * there is a security manager, its checkPermission + * method is called with an AWTPermission("readDisplayPixels") + * permission. + * + * @param comp the Composite object to be used for rendering + * @throws SecurityException + * if a custom Composite object is being + * used to render to the screen and a security manager + * is set and its checkPermission method + * does not allow the operation. + * @see java.awt.Graphics#setXORMode + * @see java.awt.Graphics#setPaintMode + * @see java.awt.AlphaComposite + */ + public void setComposite(Composite comp) { + this.composite = comp; + } + + + /** + * Sets the Paint attribute for the + * Graphics2D context. Calling this method + * with a null Paint object does + * not have any effect on the current Paint attribute + * of this Graphics2D. + * @param paint the Paint object to be used to generate + * color during the rendering process, or null + * @see java.awt.Graphics#setColor + * @see java.awt.GradientPaint + * @see java.awt.TexturePaint + */ + public void setPaint(Paint paint) { + if (paint == null) { + return; + } + + this.paint = paint; + if (paint instanceof Color) { + foreground = (Color)paint; + } else { + // use default; otherwise the previous Color will be used + foreground = Color.black; + } + } + + + /** + * Sets the Stroke for the Graphics2D context. + * @param s the Stroke object to be used to stroke a + * Shape during the rendering process + * @see BasicStroke + */ + public void setStroke(Stroke s) { + stroke = s; + } + + /** + * Sets the value of a single preference for the rendering algorithms. + * Hint categories include controls for rendering quality and overall + * time/quality trade-off in the rendering process. Refer to the + * RenderingHints class for definitions of some common + * keys and values. + * @param hintKey the key of the hint to be set. + * @param hintValue the value indicating preferences for the specified + * hint category. + * @see RenderingHints + */ + public void setRenderingHint(RenderingHints.Key hintKey, Object hintValue) { + hints.put(hintKey, hintValue); + } + + + /** + * Returns the value of a single preference for the rendering algorithms. + * Hint categories include controls for rendering quality and overall + * time/quality trade-off in the rendering process. Refer to the + * RenderingHints class for definitions of some common + * keys and values. + * @param hintKey the key corresponding to the hint to get. + * @return an object representing the value for the specified hint key. + * Some of the keys and their associated values are defined in the + * RenderingHints class. + * @see RenderingHints + */ + public Object getRenderingHint(RenderingHints.Key hintKey) { + return hints.get(hintKey); + } + + + /** + * Replaces the values of all preferences for the rendering + * algorithms with the specified hints. + * The existing values for all rendering hints are discarded and + * the new set of known hints and values are initialized from the + * specified {@link Map} object. + * Hint categories include controls for rendering quality and + * overall time/quality trade-off in the rendering process. + * Refer to the RenderingHints class for definitions of + * some common keys and values. + * @param hints the rendering hints to be set + * @see RenderingHints + */ + public void setRenderingHints(Map hints) { + this.hints = new RenderingHints(hints); + } + + + /** + * Sets the values of an arbitrary number of preferences for the + * rendering algorithms. + * Only values for the rendering hints that are present in the + * specified Map object are modified. + * All other preferences not present in the specified + * object are left unmodified. + * Hint categories include controls for rendering quality and + * overall time/quality trade-off in the rendering process. + * Refer to the RenderingHints class for definitions of + * some common keys and values. + * @param hints the rendering hints to be set + * @see RenderingHints + */ + public void addRenderingHints(Map hints) { + this.hints.putAll(hints); + } + + + /** + * Gets the preferences for the rendering algorithms. Hint categories + * include controls for rendering quality and overall time/quality + * trade-off in the rendering process. + * Returns all of the hint key/value pairs that were ever specified in + * one operation. Refer to the + * RenderingHints class for definitions of some common + * keys and values. + * @return a reference to an instance of RenderingHints + * that contains the current preferences. + * @see RenderingHints + */ + public RenderingHints getRenderingHints() { + return hints; + } + + /** + * Translates the origin of the graphics context to the point + * (xy) in the current coordinate system. + * Modifies this graphics context so that its new origin corresponds + * to the point (xy) in this graphics context's + * original coordinate system. All coordinates used in subsequent + * rendering operations on this graphics context will be relative + * to this new origin. + * @param x the x coordinate. + * @param y the y coordinate. + */ + public void translate(int x, int y) { + if (x != 0 || y != 0) { + transform.translate(x, y); + transformStack.add(TransformStackElement.createTranslateElement(x, y)); + } + } + + + /** + * Concatenates the current + * Graphics2D Transform + * with a translation transform. + * Subsequent rendering is translated by the specified + * distance relative to the previous position. + * This is equivalent to calling transform(T), where T is an + * AffineTransform represented by the following matrix: + *

    +     *          [   1    0    tx  ]
    +     *          [   0    1    ty  ]
    +     *          [   0    0    1   ]
    +     * 
    + * @param tx the distance to translate along the x-axis + * @param ty the distance to translate along the y-axis + */ + public void translate(double tx, double ty) { + transform.translate(tx, ty); + transformStack.add(TransformStackElement.createTranslateElement(tx, ty)); + } + + /** + * Concatenates the current Graphics2D + * Transform with a rotation transform. + * Subsequent rendering is rotated by the specified radians relative + * to the previous origin. + * This is equivalent to calling transform(R), where R is an + * AffineTransform represented by the following matrix: + *
    +     *          [   cos(theta)    -sin(theta)    0   ]
    +     *          [   sin(theta)     cos(theta)    0   ]
    +     *          [       0              0         1   ]
    +     * 
    + * Rotating with a positive angle theta rotates points on the positive + * x axis toward the positive y axis. + * @param theta the angle of rotation in radians + */ + public void rotate(double theta) { + transform.rotate(theta); + transformStack.add(TransformStackElement.createRotateElement(theta)); + } + + /** + * Concatenates the current Graphics2D + * Transform with a translated rotation + * transform. Subsequent rendering is transformed by a transform + * which is constructed by translating to the specified location, + * rotating by the specified radians, and translating back by the same + * amount as the original translation. This is equivalent to the + * following sequence of calls: + *
    +     *          translate(x, y);
    +     *          rotate(theta);
    +     *          translate(-x, -y);
    +     * 
    + * Rotating with a positive angle theta rotates points on the positive + * x axis toward the positive y axis. + * @param theta the angle of rotation in radians + * @param x x coordinate of the origin of the rotation + * @param y y coordinate of the origin of the rotation + */ + public void rotate(double theta, double x, double y) { + transform.rotate(theta, x, y); + transformStack.add(TransformStackElement.createTranslateElement(x, y)); + transformStack.add(TransformStackElement.createRotateElement(theta)); + transformStack.add(TransformStackElement.createTranslateElement(-x, -y)); + } + + /** + * Concatenates the current Graphics2D + * Transform with a scaling transformation + * Subsequent rendering is resized according to the specified scaling + * factors relative to the previous scaling. + * This is equivalent to calling transform(S), where S is an + * AffineTransform represented by the following matrix: + *
    +     *          [   sx   0    0   ]
    +     *          [   0    sy   0   ]
    +     *          [   0    0    1   ]
    +     * 
    + * @param sx the amount by which X coordinates in subsequent + * rendering operations are multiplied relative to previous + * rendering operations. + * @param sy the amount by which Y coordinates in subsequent + * rendering operations are multiplied relative to previous + * rendering operations. + */ + public void scale(double sx, double sy) { + transform.scale(sx, sy); + transformStack.add(TransformStackElement.createScaleElement(sx, sy)); + } + + /** + * Concatenates the current Graphics2D + * Transform with a shearing transform. + * Subsequent renderings are sheared by the specified + * multiplier relative to the previous position. + * This is equivalent to calling transform(SH), where SH + * is an AffineTransform represented by the following + * matrix: + *
    +     *          [   1   shx   0   ]
    +     *          [  shy   1    0   ]
    +     *          [   0    0    1   ]
    +     * 
    + * @param shx the multiplier by which coordinates are shifted in + * the positive X axis direction as a function of their Y coordinate + * @param shy the multiplier by which coordinates are shifted in + * the positive Y axis direction as a function of their X coordinate + */ + public void shear(double shx, double shy) { + transform.shear(shx, shy); + transformStack.add(TransformStackElement.createShearElement(shx, shy)); + } + + /** + * Composes an AffineTransform object with the + * Transform in this Graphics2D according + * to the rule last-specified-first-applied. If the current + * Transform is Cx, the result of composition + * with Tx is a new Transform Cx'. Cx' becomes the + * current Transform for this Graphics2D. + * Transforming a point p by the updated Transform Cx' is + * equivalent to first transforming p by Tx and then transforming + * the result by the original Transform Cx. In other + * words, Cx'(p) = Cx(Tx(p)). A copy of the Tx is made, if necessary, + * so further modifications to Tx do not affect rendering. + * @param tx the AffineTransform object to be composed with + * the current Transform + * @see #setTransform + * @see AffineTransform + */ + public void transform(AffineTransform tx) { + transform.concatenate(tx); + transformStack.add(TransformStackElement.createGeneralTransformElement(tx)); + } + + /** + * Sets the Transform in the Graphics2D + * context. + * @param tx the AffineTransform object to be used in the + * rendering process + * @see #transform + * @see AffineTransform + */ + public void setTransform(AffineTransform tx) { + transform = new AffineTransform(tx); + invalidateTransformStack(); + if (!tx.isIdentity()) { + transformStack.add(TransformStackElement.createGeneralTransformElement(tx)); + } + } + + /** + * Marks the GraphicContext's isNewTransformStack to false + * as a memento that the current transform stack was read and + * has not been reset. Only the setTransform method can + * override this memento. + */ + public void validateTransformStack() { + transformStackValid = true; + } + + /** + * Checks the status of the transform stack. + * @return true if the transform stack is valid + */ + public boolean isTransformStackValid() { + return transformStackValid; + } + + /** + * @return array containing the successive transforms that + * were concatenated with the original one. + */ + public TransformStackElement[] getTransformStack() { + TransformStackElement[] stack = new TransformStackElement[transformStack.size()]; + transformStack.toArray(stack); + return stack; + } + + /** + * Marks the GraphicContext's isNewTransformStack to true + * as a memento that the current transform stack was reset + * since it was last read. Only validateTransformStack + * can override this memento + */ + protected void invalidateTransformStack() { + transformStack.clear(); + transformStackValid = false; + } + + /** + * Returns a copy of the current Transform in the + * Graphics2D context. + * @return the current AffineTransform in the + * Graphics2D context. + * @see #transform + * @see #setTransform + */ + public AffineTransform getTransform() { + return new AffineTransform(transform); + } + + /** + * Returns the current Paint of the + * Graphics2D context. + * @return the current Graphics2D Paint, + * which defines a color or pattern. + * @see #setPaint + * @see java.awt.Graphics#setColor + */ + public Paint getPaint() { + return paint; + } + + + /** + * Returns the current Composite in the + * Graphics2D context. + * @return the current Graphics2D Composite, + * which defines a compositing style. + * @see #setComposite + */ + public Composite getComposite() { + return composite; + } + + /** + * Sets the background color for the Graphics2D context. + * The background color is used for clearing a region. + * When a Graphics2D is constructed for a + * Component, the background color is + * inherited from the Component. Setting the background color + * in the Graphics2D context only affects the subsequent + * clearRect calls and not the background color of the + * Component. To change the background + * of the Component, use appropriate methods of + * the Component. + * @param color the background color that isused in + * subsequent calls to clearRect + * @see #getBackground + * @see java.awt.Graphics#clearRect + */ + public void setBackground(Color color) { + if (color == null) { + return; + } + + background = color; + } + + + /** + * Returns the background color used for clearing a region. + * @return the current Graphics2D Color, + * which defines the background color. + * @see #setBackground + */ + public Color getBackground() { + return background; + } + + /** + * Returns the current Stroke in the + * Graphics2D context. + * @return the current Graphics2D Stroke, + * which defines the line style. + * @see #setStroke + */ + public Stroke getStroke() { + return stroke; + } + + + /** + * Intersects the current Clip with the interior of the + * specified Shape and sets the Clip to the + * resulting intersection. The specified Shape is + * transformed with the current Graphics2D + * Transform before being intersected with the current + * Clip. This method is used to make the current + * Clip smaller. + * To make the Clip larger, use setClip. + * The user clip modified by this method is independent of the + * clipping associated with device bounds and visibility. If no clip has + * previously been set, or if the clip has been cleared using + * {@link java.awt.Graphics#setClip(Shape) setClip} with a + * null argument, the specified Shape becomes + * the new user clip. + * @param s the Shape to be intersected with the current + * Clip. If s is null, + * this method clears the current Clip. + */ + public void clip(Shape s) { + if (s != null) { + s = transform.createTransformedShape(s); + } + + if (clip != null) { + Area newClip = new Area(clip); + newClip.intersect(new Area(s)); + clip = new GeneralPath(newClip); + } else { + clip = s; + } + } + + /** + * Get the rendering context of the Font within this + * Graphics2D context. + * The {@link FontRenderContext} + * encapsulates application hints such as anti-aliasing and + * fractional metrics, as well as target device specific information + * such as dots-per-inch. This information should be provided by the + * application when using objects that perform typographical + * formatting, such as Font and + * TextLayout. This information should also be provided + * by applications that perform their own layout and need accurate + * measurements of various characteristics of glyphs such as advance + * and line height when various rendering hints have been applied to + * the text rendering. + * + * @return a reference to an instance of FontRenderContext. + * @see java.awt.font.FontRenderContext + * @see java.awt.Font#createGlyphVector(FontRenderContext,char[]) + * @see java.awt.font.TextLayout + * @since JDK1.2 + */ + public FontRenderContext getFontRenderContext() { + // + // Find if antialiasing should be used. + // + Object antialiasingHint = hints.get(RenderingHints.KEY_TEXT_ANTIALIASING); + boolean isAntialiased = true; + if (antialiasingHint != RenderingHints.VALUE_TEXT_ANTIALIAS_ON + && antialiasingHint != RenderingHints.VALUE_TEXT_ANTIALIAS_DEFAULT) { + + // If antialias was not turned off, then use the general rendering + // hint. + if (antialiasingHint != RenderingHints.VALUE_TEXT_ANTIALIAS_OFF) { + antialiasingHint = hints.get(RenderingHints.KEY_ANTIALIASING); + + // Test general hint + if (antialiasingHint != RenderingHints.VALUE_ANTIALIAS_ON + && antialiasingHint != RenderingHints.VALUE_ANTIALIAS_DEFAULT) { + // Antialiasing was not requested. However, if it was not turned + // off explicitly, use it. + if (antialiasingHint == RenderingHints.VALUE_ANTIALIAS_OFF) { + isAntialiased = false; + } + } + } else { + isAntialiased = false; + } + + } + + // + // Find out whether fractional metrics should be used. + // + boolean useFractionalMetrics = true; + if (hints.get(RenderingHints.KEY_FRACTIONALMETRICS) + == RenderingHints.VALUE_FRACTIONALMETRICS_OFF) { + useFractionalMetrics = false; + } + + FontRenderContext frc = new FontRenderContext(defaultTransform, + isAntialiased, + useFractionalMetrics); + return frc; + } +} diff --git a/src/main/java/org/apache/xmlgraphics/java2d/Graphics2DImagePainter.java b/src/main/java/org/apache/xmlgraphics/java2d/Graphics2DImagePainter.java new file mode 100644 index 0000000..3343984 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/java2d/Graphics2DImagePainter.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: Graphics2DImagePainter.java 1345683 2012-06-03 14:50:33Z gadams $ */ + +package org.apache.xmlgraphics.java2d; + +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.geom.Rectangle2D; + +/** + * This interface is used to paint vector graphic images. Components that can paint using + * a Graphics2D instance (i.e. Java2D) can implement this interface to paint themselves. + */ +public interface Graphics2DImagePainter { + + /** + * Called to paint the image. Implementations should scale so the image is + * painted fully inside the given area indicated by then Rectangle2D object. + * @param g2d the Graphics2D instance to paint on + * @param area the target area for the image (in target device units) + */ + void paint(Graphics2D g2d, Rectangle2D area); + + /** + * @return the dimensions (intrinsic size) of the image to be painted in millipoints + */ + Dimension getImageSize(); + +} diff --git a/src/main/java/org/apache/xmlgraphics/java2d/GraphicsConfigurationWithTransparency.java b/src/main/java/org/apache/xmlgraphics/java2d/GraphicsConfigurationWithTransparency.java new file mode 100644 index 0000000..830bb63 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/java2d/GraphicsConfigurationWithTransparency.java @@ -0,0 +1,129 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: GraphicsConfigurationWithTransparency.java 1862543 2019-07-04 09:25:38Z ssteiner $ */ + +package org.apache.xmlgraphics.java2d; + +import java.awt.GraphicsDevice; +import java.awt.Rectangle; +import java.awt.Transparency; +import java.awt.geom.AffineTransform; +import java.awt.image.BufferedImage; +import java.awt.image.ColorModel; + +/** + * An implementation of {@link java.awt.GraphicsConfiguration} that supports transparencies (alpha + * channels). + */ +public class GraphicsConfigurationWithTransparency extends AbstractGraphicsConfiguration { + // We use this to get a good colormodel.. + private static final BufferedImage BI_WITH_ALPHA = + new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB); + // We use this to get a good colormodel.. + private static final BufferedImage BI_WITHOUT_ALPHA = + new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB); + + /** + * Construct a buffered image with an alpha channel, unless transparency is OPAQUE (no alpha + * at all). + * + * @param width the width of the image + * @param height the height of the image + * @param transparency the alpha value of the image + * @return the new buffered image + */ + public BufferedImage createCompatibleImage(int width, int height, int transparency) { + if (transparency == Transparency.OPAQUE) { + return new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + } else { + return new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + } + } + + /** + * Construct a buffered image with an alpha channel. + * + * @param width the width of the image + * @param height the height of the image + * @return the new buffered image + */ + public BufferedImage createCompatibleImage(int width, int height) { + return new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + } + + /** + * TODO: This should return the page bounds in Pts, I couldn't figure out how to get this for + * the current page (this still works for now, but it should be fixed...). + * + * @return the bounds of the document page + */ + public Rectangle getBounds() { + return new Rectangle(); + } + + /** + * Return a good default color model for this 'device'. + * @return the colour model for the configuration + */ + public ColorModel getColorModel() { + return BI_WITH_ALPHA.getColorModel(); + } + + /** + * Return a good color model given transparency + * + * @param transparency the alpha value for the colour model + * @return the colour model for the configuration + */ + public ColorModel getColorModel(int transparency) { + if (transparency == Transparency.OPAQUE) { + return BI_WITHOUT_ALPHA.getColorModel(); + } else { + return BI_WITH_ALPHA.getColorModel(); + } + } + + /** + * The default transform (1:1). + * + * @return the default transform for the configuration + */ + public AffineTransform getDefaultTransform() { + return new AffineTransform(); + } + + /** + * The normalizing transform (1:1) (since we currently + * render images at 72dpi, which we might want to change + * in the future). + * + * @return the normalizing transform for the configuration + */ + public AffineTransform getNormalizingTransform() { + return new AffineTransform(2, 0, 0, 2, 0, 0); + } + + /** + * Return our dummy instance of GraphicsDevice + * + * @return the graphics device + */ + public GraphicsDevice getDevice() { + return new GenericGraphicsDevice(this); + } +} diff --git a/src/main/java/org/apache/xmlgraphics/java2d/GraphicsConfigurationWithoutTransparency.java b/src/main/java/org/apache/xmlgraphics/java2d/GraphicsConfigurationWithoutTransparency.java new file mode 100644 index 0000000..35494b8 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/java2d/GraphicsConfigurationWithoutTransparency.java @@ -0,0 +1,89 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.xmlgraphics.java2d; + +import java.awt.GraphicsDevice; +import java.awt.Rectangle; +import java.awt.Transparency; +import java.awt.geom.AffineTransform; +import java.awt.image.BufferedImage; +import java.awt.image.ColorModel; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * An implementation of {@link java.awt.GraphicsConfiguration} that does not support transparencies + * (alpha channels). + */ +public class GraphicsConfigurationWithoutTransparency extends AbstractGraphicsConfiguration { + + private static final Log LOG = LogFactory.getLog(GraphicsConfigurationWithoutTransparency.class); + + // We use this to get a good colormodel.. + private static final BufferedImage BI_WITHOUT_ALPHA = + new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB); + + private final GraphicsConfigurationWithTransparency defaultDelegate = new GraphicsConfigurationWithTransparency(); + + @Override + public GraphicsDevice getDevice() { + return new GenericGraphicsDevice(this); + } + + @Override + public BufferedImage createCompatibleImage(int width, int height) { + return defaultDelegate.createCompatibleImage(width, height, Transparency.OPAQUE); + } + + @Override + public BufferedImage createCompatibleImage(int width, int height, int transparency) { + if (transparency != Transparency.OPAQUE) { + LOG.warn("Does not support transparencies (alpha channels) in images"); + } + return defaultDelegate.createCompatibleImage(width, height, Transparency.OPAQUE); + } + + @Override + public ColorModel getColorModel() { + return BI_WITHOUT_ALPHA.getColorModel(); + } + + @Override + public ColorModel getColorModel(int transparency) { + if (transparency == Transparency.OPAQUE) { + LOG.warn("Does not support transparencies (alpha channels) in images"); + } + return getColorModel(); + } + + @Override + public AffineTransform getDefaultTransform() { + return defaultDelegate.getDefaultTransform(); + } + + @Override + public AffineTransform getNormalizingTransform() { + return defaultDelegate.getNormalizingTransform(); + } + + @Override + public Rectangle getBounds() { + return new Rectangle(); + } +} diff --git a/src/main/java/org/apache/xmlgraphics/java2d/StrokingTextHandler.java b/src/main/java/org/apache/xmlgraphics/java2d/StrokingTextHandler.java new file mode 100644 index 0000000..5bc9045 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/java2d/StrokingTextHandler.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: StrokingTextHandler.java 995366 2010-09-09 10:02:17Z jeremias $ */ + +package org.apache.xmlgraphics.java2d; + +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.font.FontRenderContext; +import java.awt.font.GlyphVector; +import java.io.IOException; + +/** + * Default TextHandler implementation which paints text using graphics primitives (shapes). + */ +public class StrokingTextHandler implements TextHandler { + + /** + * Default constructor. + */ + public StrokingTextHandler() { + //nop + } + + /** {@inheritDoc} */ + public void drawString(Graphics2D g2d, String text, float x, float y) throws IOException { + java.awt.Font awtFont = g2d.getFont(); + FontRenderContext frc = g2d.getFontRenderContext(); + GlyphVector gv = awtFont.createGlyphVector(frc, text); + Shape glyphOutline = gv.getOutline(x, y); + g2d.fill(glyphOutline); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/java2d/TextHandler.java b/src/main/java/org/apache/xmlgraphics/java2d/TextHandler.java new file mode 100644 index 0000000..e2a5427 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/java2d/TextHandler.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: TextHandler.java 995366 2010-09-09 10:02:17Z jeremias $ */ + +package org.apache.xmlgraphics.java2d; + +import java.awt.Graphics2D; +import java.io.IOException; + +/** + * Interface which the Graphics2D class delegates text painting to. + */ +public interface TextHandler { + + /** + * Draw some text. + * @param g2d the graphics 2D implementation + * @param text the text to paint + * @param x the x-coordinate where the String should be rendered + * @param y the y-coordinate where the String should be rendered + * @throws IOException In case of an I/O error + */ + void drawString(Graphics2D g2d, String text, float x, float y) throws IOException; + +} diff --git a/src/main/java/org/apache/xmlgraphics/java2d/TransformStackElement.java b/src/main/java/org/apache/xmlgraphics/java2d/TransformStackElement.java new file mode 100644 index 0000000..a030d5e --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/java2d/TransformStackElement.java @@ -0,0 +1,214 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: TransformStackElement.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.java2d; + +import java.awt.geom.AffineTransform; + +// CSOFF: EmptyBlock +// CSOFF: NoWhitespaceAfter +// CSOFF: OperatorWrap +// CSOFF: WhitespaceAround + +/** + * Contains a description of an elementary transform stack element, + * such as a rotate or translate. A transform stack element has a + * type and a value, which is an array of double values.
    + * + * @version $Id: TransformStackElement.java 1732018 2016-02-24 04:51:06Z gadams $ + * + * Originally authored by Vincent Hardy and Paul Evenblij. + */ +public abstract class TransformStackElement implements Cloneable { + + /** + * Transform type + */ + private TransformType type; + + /** + * Value + */ + private double[] transformParameters; + + /** + * @param type transform type + * @param transformParameters parameters for transform + */ + protected TransformStackElement(TransformType type, + double[] transformParameters) { + this.type = type; + this.transformParameters = transformParameters; + } + + /** + * @return an object which is a deep copy of this one + */ + public Object clone() { + TransformStackElement newElement = null; + + // start with a shallow copy to get our implementations right + try { + newElement = (TransformStackElement) super.clone(); + } catch (java.lang.CloneNotSupportedException ex) { + throw new AssertionError(); + } + + // now deep copy the parameter array + double[] transformParameters = new double[this.transformParameters.length]; + System.arraycopy(this.transformParameters, 0, transformParameters, 0, transformParameters.length); + newElement.transformParameters = transformParameters; + return newElement; + } + + /* + * Factory methods + */ + + public static TransformStackElement createTranslateElement(double tx, + double ty) { + return new TransformStackElement(TransformType.TRANSLATE, + new double[]{ tx, ty }) { + boolean isIdentity(double[] parameters) { + return parameters[0] == 0 && parameters[1] == 0; + } + }; + } + + public static TransformStackElement createRotateElement(double theta) { + return new TransformStackElement(TransformType.ROTATE, + new double[]{ theta }) { + boolean isIdentity(double[] parameters) { + return Math.cos(parameters[0]) == 1; + } + }; + } + + public static TransformStackElement createScaleElement(double scaleX, + double scaleY) { + return new TransformStackElement(TransformType.SCALE, + new double[]{ scaleX, scaleY }) { + boolean isIdentity(double[] parameters) { + return parameters[0] == 1 && parameters[1] == 1; + } + }; + } + + public static TransformStackElement createShearElement(double shearX, + double shearY) { + return new TransformStackElement(TransformType.SHEAR, + new double[]{ shearX, shearY }) { + boolean isIdentity(double[] parameters) { + return parameters[0] == 0 && parameters[1] == 0; + } + }; + } + + public static TransformStackElement createGeneralTransformElement( + AffineTransform txf) { + double[] matrix = new double[6]; + txf.getMatrix(matrix); + return new TransformStackElement(TransformType.GENERAL, matrix) { + boolean isIdentity(double[] m) { + return (m[0] == 1 && m[2] == 0 && m[4] == 0 + && m[1] == 0 && m[3] == 1 && m[5] == 0); + } + }; + } + + /** + * Implementation should determine if the parameter list represents + * an identity transform, for the instance transform type. + */ + abstract boolean isIdentity(double[] parameters); + + /** + * @return true iff this transform is the identity transform + */ + public boolean isIdentity() { + return isIdentity(transformParameters); + } + + /** + * @return array of values containing this transform element's parameters + */ + public double[] getTransformParameters() { + return transformParameters; + } + + /** + * @return this transform type + */ + public TransformType getType() { + return type; + } + + /* + * Concatenation utility. Requests this transform stack element + * to concatenate with the input stack element. Only elements + * of the same types are concatenated. For example, if this + * element represents a translation, it will concatenate with + * another translation, but not with any other kind of + * stack element. + * @param stackElement element to be concatenated with this one. + * @return true if the input stackElement was concatenated with + * this one. False otherwise. + */ + public boolean concatenate(TransformStackElement stackElement) { + boolean canConcatenate = false; + + if (type.toInt() == stackElement.type.toInt()) { + canConcatenate = true; + switch(type.toInt()) { + case TransformType.TRANSFORM_TRANSLATE: + transformParameters[0] += stackElement.transformParameters[0]; + transformParameters[1] += stackElement.transformParameters[1]; + break; + case TransformType.TRANSFORM_ROTATE: + transformParameters[0] += stackElement.transformParameters[0]; + break; + case TransformType.TRANSFORM_SCALE: + transformParameters[0] *= stackElement.transformParameters[0]; + transformParameters[1] *= stackElement.transformParameters[1]; + break; + case TransformType.TRANSFORM_GENERAL: + transformParameters + = matrixMultiply(transformParameters, + stackElement.transformParameters); + break; + default: + canConcatenate = false; + } + } + + return canConcatenate; + } + + /** + * Multiplies two 2x3 matrices of double precision values + */ + private double[] matrixMultiply(double[] matrix1, double[] matrix2) { + double[] product = new double[6]; + AffineTransform transform1 = new AffineTransform(matrix1); + transform1.concatenate(new AffineTransform(matrix2)); + transform1.getMatrix(product); + return product; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/java2d/TransformType.java b/src/main/java/org/apache/xmlgraphics/java2d/TransformType.java new file mode 100644 index 0000000..250f8bf --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/java2d/TransformType.java @@ -0,0 +1,113 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: TransformType.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.java2d; + +// CSOFF: WhitespaceAround + +/** + * Enumeration for transformation types. + * + * @version $Id: TransformType.java 1732018 2016-02-24 04:51:06Z gadams $ + * Originally authored by Vincent Hardy. + */ +public final class TransformType { + /* + * Transform type constants + */ + public static final int TRANSFORM_TRANSLATE = 0; + public static final int TRANSFORM_ROTATE = 1; + public static final int TRANSFORM_SCALE = 2; + public static final int TRANSFORM_SHEAR = 3; + public static final int TRANSFORM_GENERAL = 4; + + /** + * Strings describing the elementary transforms + */ + public static final String TRANSLATE_STRING = "translate"; + public static final String ROTATE_STRING = "rotate"; + public static final String SCALE_STRING = "scale"; + public static final String SHEAR_STRING = "shear"; + public static final String GENERAL_STRING = "general"; + + /** + * TransformType values + */ + public static final TransformType TRANSLATE = new TransformType(TRANSFORM_TRANSLATE, TRANSLATE_STRING); + public static final TransformType ROTATE = new TransformType(TRANSFORM_ROTATE, ROTATE_STRING); + public static final TransformType SCALE = new TransformType(TRANSFORM_SCALE, SCALE_STRING); + public static final TransformType SHEAR = new TransformType(TRANSFORM_SHEAR, SHEAR_STRING); + public static final TransformType GENERAL = new TransformType(TRANSFORM_GENERAL, GENERAL_STRING); + + private String desc; + private int val; + + /** + * Constructor is private so that no instances other than + * the ones in the enumeration can be created. + * @see #readResolve + */ + private TransformType(int val, String desc) { + this.desc = desc; + this.val = val; + } + + /** + * @return description + */ + public String toString() { + return desc; + } + + /** + * Convenience for enumeration switching. + * That is, + *
    +     *   switch(transformType.toInt()){
    +     *       case TransformType.TRANSFORM_TRANSLATE:
    +     *        ....
    +     *       case TransformType.TRANSFORM_ROTATE:
    +     * 
    + */ + public int toInt() { + return val; + } + + /** + * This is called by the serialization code before it returns an unserialized + * object. To provide for unicity of instances, the instance that was read + * is replaced by its static equivalent + */ + public Object readResolve() { + switch(val) { + case TRANSFORM_TRANSLATE: + return TransformType.TRANSLATE; + case TRANSFORM_ROTATE: + return TransformType.ROTATE; + case TRANSFORM_SCALE: + return TransformType.SCALE; + case TRANSFORM_SHEAR: + return TransformType.SHEAR; + case TRANSFORM_GENERAL: + return TransformType.GENERAL; + default: + throw new RuntimeException("Unknown TransformType value"); + } + } +} diff --git a/src/main/java/org/apache/xmlgraphics/java2d/color/AbstractDeviceSpecificColorSpace.java b/src/main/java/org/apache/xmlgraphics/java2d/color/AbstractDeviceSpecificColorSpace.java new file mode 100644 index 0000000..ac8aa98 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/java2d/color/AbstractDeviceSpecificColorSpace.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: AbstractDeviceSpecificColorSpace.java 1051421 2010-12-21 08:54:25Z jeremias $ */ + +package org.apache.xmlgraphics.java2d.color; + +import java.awt.color.ColorSpace; + +/** + * Base class for device-specific (uncalibrated) color spaces. + */ +public abstract class AbstractDeviceSpecificColorSpace extends ColorSpace { + + private static final long serialVersionUID = -4888985582872101875L; + + /** + * Creates a new instance. + * @param type the color space type ({@link ColorSpace}.TYPE_*) + * @param numcomponents the number of color components applicable to the color space + */ + protected AbstractDeviceSpecificColorSpace(int type, int numcomponents) { + super(type, numcomponents); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/java2d/color/CIELabColorSpace.java b/src/main/java/org/apache/xmlgraphics/java2d/color/CIELabColorSpace.java new file mode 100644 index 0000000..4ce1c79 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/java2d/color/CIELabColorSpace.java @@ -0,0 +1,322 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: CIELabColorSpace.java 1051421 2010-12-21 08:54:25Z jeremias $ */ + +package org.apache.xmlgraphics.java2d.color; + +import java.awt.Color; +import java.awt.color.ColorSpace; + +/** + * This class defines the CIE L*a*b* (CIE 1976) color space. Valid values for L* are between 0 + * and 100, for a* and b* between -127 and +127. + * @see http://en.wikipedia.org/wiki/Lab_color_space + */ +public class CIELabColorSpace extends ColorSpace { + + private static final long serialVersionUID = -1821569090707520704L; + + //CIE XYZ tristimulus values of the reference white point: Observer= 2 degrees, Illuminant= D65 + private static final float REF_X_D65 = 95.047f; + private static final float REF_Y_D65 = 100.000f; + private static final float REF_Z_D65 = 108.883f; + + //CIE XYZ tristimulus values of the reference white point: Illuminant= D50 + private static final float REF_X_D50 = 96.42f; + private static final float REF_Y_D50 = 100.00f; + private static final float REF_Z_D50 = 82.49f; + + private static final double D = 6.0 / 29.0; + private static final double REF_A = 1.0 / (3 * Math.pow(D, 2)); //7.787037... + private static final double REF_B = 16.0 / 116.0; + private static final double T0 = Math.pow(D, 3); //0.008856... + + private float wpX; + private float wpY; + private float wpZ; + + /** + * Default constructor using the D65 white point. + */ + public CIELabColorSpace() { + this(getD65WhitePoint()); + } + + /** + * CIE Lab space constructor which allows to give an arbitrary white point. + * @param whitePoint the white point in XYZ coordinates (valid values: 0.0f to 1.0f, although + * values slightly larger than 1.0f are common) + */ + public CIELabColorSpace(float[] whitePoint) { + super(ColorSpace.TYPE_Lab, 3); + checkNumComponents(whitePoint, 3); + this.wpX = whitePoint[0]; + this.wpY = whitePoint[1]; + this.wpZ = whitePoint[2]; + } + + /** + * Returns the D65 white point. + * @return the D65 white point. + */ + public static float[] getD65WhitePoint() { + return new float[] {REF_X_D65, REF_Y_D65, REF_Z_D65}; + } + + /** + * Returns the D50 white point. + * @return the D50 white point. + */ + public static float[] getD50WhitePoint() { + return new float[] {REF_X_D50, REF_Y_D50, REF_Z_D50}; + } + + private void checkNumComponents(float[] colorvalue) { + checkNumComponents(colorvalue, getNumComponents()); + } + + private void checkNumComponents(float[] colorvalue, int expected) { + if (colorvalue == null) { + throw new NullPointerException("color value may not be null"); + } + if (colorvalue.length != expected) { + throw new IllegalArgumentException("Expected " + expected + + " components, but got " + colorvalue.length); + } + } + + /** + * Returns the configured white point. + * @return the white point in CIE XYZ coordinates + */ + public float[] getWhitePoint() { + return new float[] {wpX, wpY, wpZ}; + } + + private static final String CIE_LAB_ONLY_HAS_3_COMPONENTS = "CIE Lab only has 3 components!"; + + /** {@inheritDoc} */ + @Override + public float getMinValue(int component) { + switch (component) { + case 0: //L* + return 0f; + case 1: //a* + case 2: //b* + return -128f; + default: + throw new IllegalArgumentException(CIE_LAB_ONLY_HAS_3_COMPONENTS); + } + } + + /** {@inheritDoc} */ + @Override + public float getMaxValue(int component) { + switch (component) { + case 0: //L* + return 100f; + case 1: //a* + case 2: //b* + return 128f; + default: + throw new IllegalArgumentException(CIE_LAB_ONLY_HAS_3_COMPONENTS); + } + } + + /** {@inheritDoc} */ + @Override + public String getName(int component) { + switch (component) { + case 0: + return "L*"; + case 1: + return "a*"; + case 2: + return "b*"; + default: + throw new IllegalArgumentException(CIE_LAB_ONLY_HAS_3_COMPONENTS); + } + } + + //Note: the conversion functions used here were mostly borrowed from Apache Commons Sanselan + //and adjusted to the local requirements. + + /** {@inheritDoc} */ + @Override + public float[] fromCIEXYZ(float[] colorvalue) { + checkNumComponents(colorvalue, 3); + float x = colorvalue[0]; + float y = colorvalue[1]; + float z = colorvalue[2]; + + double varX = x / wpX; + double varY = y / wpY; + double varZ = z / wpZ; + + if (varX > T0) { + varX = Math.pow(varX, (1 / 3.0)); + } else { + varX = (REF_A * varX) + REF_B; + } + if (varY > T0) { + varY = Math.pow(varY, 1 / 3.0); + } else { + varY = (REF_A * varY) + REF_B; + } + if (varZ > T0) { + varZ = Math.pow(varZ, 1 / 3.0); + } else { + varZ = (REF_A * varZ) + REF_B; + } + + float l = (float)((116 * varY) - 16); + float a = (float)(500 * (varX - varY)); + float b = (float)(200 * (varY - varZ)); + + //Normalize to range 0.0..1.0 + l = normalize(l, 0); + a = normalize(a, 1); + b = normalize(b, 2); + return new float[] {l, a, b}; + } + + /** {@inheritDoc} */ + @Override + public float[] fromRGB(float[] rgbvalue) { + ColorSpace sRGB = ColorSpace.getInstance(ColorSpace.CS_sRGB); + float[] xyz = sRGB.toCIEXYZ(rgbvalue); + return fromCIEXYZ(xyz); + } + + /** {@inheritDoc} */ + @Override + public float[] toCIEXYZ(float[] colorvalue) { + checkNumComponents(colorvalue); + //Scale to native value range + float l = denormalize(colorvalue[0], 0); + float a = denormalize(colorvalue[1], 1); + float b = denormalize(colorvalue[2], 2); + + return toCIEXYZNative(l, a, b); + } + + /** + * Transforms a color value assumed to be in this {@link ColorSpace} + * into the CS_CIEXYZ conversion color space. This method uses component values + * in CIE Lab's native color ranges rather than the normalized values between 0 and 1. + * @param l the L* component (values between 0 and 100) + * @param a the a* component (usually between -128 and +128) + * @param b the b* component (usually between -128 and +128) + * @return the XYZ color values + * @see #toCIEXYZ(float[]) + */ + public float[] toCIEXYZNative(float l, float a, float b) { + double varY = (l + 16) / 116.0; + double varX = a / 500 + varY; + double varZ = varY - b / 200.0; + + if (Math.pow(varY, 3) > T0) { + varY = Math.pow(varY, 3); + } else { + varY = (varY - 16 / 116.0) / REF_A; + } + if (Math.pow(varX, 3) > T0) { + varX = Math.pow(varX, 3); + } else { + varX = (varX - 16 / 116.0) / REF_A; + } + if (Math.pow(varZ, 3) > T0) { + varZ = Math.pow(varZ, 3); + } else { + varZ = (varZ - 16 / 116.0) / REF_A; + } + + float x = (float)(wpX * varX / 100); + float y = (float)(wpY * varY / 100); + float z = (float)(wpZ * varZ / 100); + + return new float[] {x, y, z}; + } + + /** {@inheritDoc} */ + @Override + public float[] toRGB(float[] colorvalue) { + ColorSpace sRGB = ColorSpace.getInstance(ColorSpace.CS_sRGB); + float[] xyz = toCIEXYZ(colorvalue); + return sRGB.fromCIEXYZ(xyz); + } + + private float getNativeValueRange(int component) { + return getMaxValue(component) - getMinValue(component); + } + + private float normalize(float value, int component) { + return (value - getMinValue(component)) / getNativeValueRange(component); + } + + private float denormalize(float value, int component) { + return value * getNativeValueRange(component) + getMinValue(component); + } + + /** + * Converts normalized (0..1) color components to CIE L*a*b*'s native value range. + * @param comps the normalized components. + * @return the denormalized components + */ + public float[] toNativeComponents(float[] comps) { + checkNumComponents(comps); + float[] nativeComps = new float[comps.length]; + for (int i = 0, c = comps.length; i < c; i++) { + nativeComps[i] = denormalize(comps[i], i); + } + return nativeComps; + } + + /** + * Creates a {@link Color} instance from color values usually used by the L*a*b* color space + * by scaling them to the 0.0..1.0 range expected by Color's constructor. + * @param colorvalue the original color values + * (native value range, i.e. not normalized to 0.0..1.0) + * @param alpha the alpha component + * @return the requested color instance + */ + public Color toColor(float[] colorvalue, float alpha) { + int c = colorvalue.length; + float[] normalized = new float[c]; + for (int i = 0; i < c; i++) { + normalized[i] = normalize(colorvalue[i], i); + } + //Using ColorWithAlternatives for better equals() functionality + return new ColorWithAlternatives(this, normalized, alpha, null); + } + + /** + * Creates a {@link Color} instance from color values usually used by the L*a*b* color space + * by scaling them to the 0.0..1.0 range expected by Color's constructor. + * @param l the L* component (values between 0 and 100) + * @param a the a* component (usually between -128 and +127) + * @param b the b* component (usually between -128 and +127) + * @param alpha the alpha component (values between 0 and 1) + * @return the requested color instance + */ + public Color toColor(float l, float a, float b, float alpha) { + return toColor(new float[] {l, a, b}, alpha); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/java2d/color/ColorConverter.java b/src/main/java/org/apache/xmlgraphics/java2d/color/ColorConverter.java new file mode 100644 index 0000000..39fa4cc --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/java2d/color/ColorConverter.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ColorConverter.java 1358252 2012-07-06 15:03:16Z mehdi $ */ + +package org.apache.xmlgraphics.java2d.color; + +import java.awt.Color; + +/** + * Utility for implementing a color conversion scheme. + */ +public interface ColorConverter { + + /** + * @param color to convert + * @return converted color + */ + Color convert(Color color); +} diff --git a/src/main/java/org/apache/xmlgraphics/java2d/color/ColorSpaceOrigin.java b/src/main/java/org/apache/xmlgraphics/java2d/color/ColorSpaceOrigin.java new file mode 100644 index 0000000..879c5c4 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/java2d/color/ColorSpaceOrigin.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ColorSpaceOrigin.java 1598621 2014-05-30 14:55:00Z ssteiner $ */ + +package org.apache.xmlgraphics.java2d.color; + +/** + * Interface used to decorate java.awt.color.ColorSpaces subclasses + * to report the origin of the associated color profile. + */ +public interface ColorSpaceOrigin { + + /** + * Returns the name of the profile used to identify the color space in a particular context. + * @return the profile name + */ + String getProfileName(); + + /** + * Returns the URI identifying the associate color profile. + * @return the profile URI + */ + String getProfileURI(); + +} diff --git a/src/main/java/org/apache/xmlgraphics/java2d/color/ColorSpaces.java b/src/main/java/org/apache/xmlgraphics/java2d/color/ColorSpaces.java new file mode 100644 index 0000000..bf48f48 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/java2d/color/ColorSpaces.java @@ -0,0 +1,103 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ColorSpaces.java 1051421 2010-12-21 08:54:25Z jeremias $ */ + +package org.apache.xmlgraphics.java2d.color; + +import java.awt.color.ColorSpace; + +/** + * Provides access to various color spaces. + */ +public final class ColorSpaces { + + private static DeviceCMYKColorSpace deviceCMYK; + private static CIELabColorSpace cieLabD50; + private static CIELabColorSpace cieLabD65; + + private ColorSpaces() { + //Don't instantiate this class + } + + /** + * Returns an instance of the device-specific CMYK color space. + * @return an instance of the device-specific CMYK color space + */ + public static synchronized DeviceCMYKColorSpace getDeviceCMYKColorSpace() { + if (deviceCMYK == null) { + deviceCMYK = new DeviceCMYKColorSpace(); + } + return deviceCMYK; + } + + /** + * Indicates whether the given color space is device-specific (i.e. uncalibrated). + * @param cs the color space to check + * @return true if the color space is device-specific + */ + public static boolean isDeviceColorSpace(ColorSpace cs) { + return (cs instanceof AbstractDeviceSpecificColorSpace); + } + + /** + * Returns an instance of the CIE L*a*b* color space using the D50 white point. + * @return an instance of the requested CIE L*a*b* color space + */ + public static synchronized CIELabColorSpace getCIELabColorSpaceD50() { + if (cieLabD50 == null) { + cieLabD50 = new CIELabColorSpace(CIELabColorSpace.getD50WhitePoint()); + } + return cieLabD50; + } + + /** + * Returns an instance of the CIE L*a*b* color space using the D65 white point. + * @return an instance of the requested CIE L*a*b* color space + */ + public static synchronized CIELabColorSpace getCIELabColorSpaceD65() { + if (cieLabD65 == null) { + cieLabD65 = new CIELabColorSpace(CIELabColorSpace.getD65WhitePoint()); + } + return cieLabD65; + } + + private static final ColorSpaceOrigin UNKNOWN_ORIGIN = new ColorSpaceOrigin() { + + public String getProfileURI() { + return null; + } + + public String getProfileName() { + return null; + } + }; + + /** + * Returns information about the origin of a color space. + * @param cs the color space + * @return the origin information + */ + public static ColorSpaceOrigin getColorSpaceOrigin(ColorSpace cs) { + if (cs instanceof ColorSpaceOrigin) { + return (ColorSpaceOrigin)cs; + } else { + return UNKNOWN_ORIGIN; + } + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/java2d/color/ColorUtil.java b/src/main/java/org/apache/xmlgraphics/java2d/color/ColorUtil.java new file mode 100644 index 0000000..d1f6070 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/java2d/color/ColorUtil.java @@ -0,0 +1,165 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ColorUtil.java 1051421 2010-12-21 08:54:25Z jeremias $ */ + +package org.apache.xmlgraphics.java2d.color; + +import java.awt.Color; + + +/** + * Generic Color helper class. + *

    + * This class supports parsing string values into color values and creating + * color values for strings. It provides a list of standard color names. + */ +public final class ColorUtil { + + /** + * Private constructor since this is an utility class. + */ + private ColorUtil() { + } + + + /** + * Lightens up a color for groove, ridge, inset and outset border effects. + * @param col the color to lighten up + * @param factor factor by which to lighten up (negative values darken the color) + * @return the modified color + */ + public static Color lightenColor(Color col, float factor) { + // TODO: This function converts the color into the sRGB namespace. + // This should be avoided if possible. + float[] cols = new float[4]; + cols = col.getRGBComponents(cols); + if (factor > 0) { + cols[0] += (1.0 - cols[0]) * factor; + cols[1] += (1.0 - cols[1]) * factor; + cols[2] += (1.0 - cols[2]) * factor; + } else { + cols[0] -= cols[0] * -factor; + cols[1] -= cols[1] * -factor; + cols[2] -= cols[2] * -factor; + } + return new Color(cols[0], cols[1], cols[2], cols[3]); + } + + + + /** + * Indicates whether the color is a gray value. + * @param col the color + * @return true if it is a gray value + */ + public static boolean isGray(Color col) { + return (col.getRed() == col.getBlue() && col.getRed() == col.getGreen()); + } + + /** + * Creates an uncalibrated CMYK color with the given gray value. + * @param black the gray component (0 - 1) + * @return the CMYK color + */ + public static Color toCMYKGrayColor(float black) { + //Calculated color components + float[] cmyk = new float[] {0f, 0f, 0f, 1.0f - black}; + //Create native color + return DeviceCMYKColorSpace.createCMYKColor(cmyk); + } + + /** + * Converts an arbitrary {@link Color} to a plain sRGB color doing the conversion at the + * best possible conversion quality. + * @param col the original color + * @return the sRGB equivalent + */ + public static Color toSRGBColor(Color col) { + if (col.getColorSpace().isCS_sRGB()) { + return col; //Don't convert if already sRGB to avoid conversion differences + } + float[] comps = col.getColorComponents(null); + float[] srgb = col.getColorSpace().toRGB(comps); + comps = col.getComponents(null); + float alpha = comps[comps.length - 1]; + return new Color(srgb[0], srgb[1], srgb[2], alpha); + } + + /** + * Checks if two colors are the same color. This check is much more restrictive than + * {@link Color#equals(Object)} in that it doesn't only check if both colors result in the + * same sRGB value. For example, if two colors not of the same exact class are compared, + * they are treated as not the same. + *

    + * Note: At the moment, this method only supports {@link Color} and + * {@link ColorWithAlternatives} only. Other subclasses of {@link Color} are checked only using + * the {@link Color#equals(Object)} method. + * @param col1 the first color + * @param col2 the second color + * @return true if both colors are the same color + */ + public static boolean isSameColor(Color col1, Color col2) { + //Check fallback sRGB values first, then go into details + if (!col1.equals(col2)) { + return false; + } + + //Consider same-ness only between colors of the same class (not subclasses) + //but consider a ColorWithAlternatives without alternatives to be the same as a Color. + Class cl1 = col1.getClass(); + if (col1 instanceof ColorWithAlternatives + && !((ColorWithAlternatives) col1).hasAlternativeColors()) { + cl1 = Color.class; + } + Class cl2 = col2.getClass(); + if (col2 instanceof ColorWithAlternatives + && !((ColorWithAlternatives) col2).hasAlternativeColors()) { + cl2 = Color.class; + } + if (cl1 != cl2) { + return false; + } + + //Check color space + if (!col1.getColorSpace().equals(col2.getColorSpace())) { + return false; + } + + //Check native components + float[] comps1 = col1.getComponents(null); + float[] comps2 = col2.getComponents(null); + if (comps1.length != comps2.length) { + return false; + } + for (int i = 0, c = comps1.length; i < c; i++) { + if (comps1[i] != comps2[i]) { + return false; + } + } + + //Compare alternative colors, order is relevant + if (col1 instanceof ColorWithAlternatives && col2 instanceof ColorWithAlternatives) { + ColorWithAlternatives ca1 = (ColorWithAlternatives) col1; + ColorWithAlternatives ca2 = (ColorWithAlternatives) col2; + return ca1.hasSameAlternativeColors(ca2); + } + + return true; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/java2d/color/ColorWithAlternatives.java b/src/main/java/org/apache/xmlgraphics/java2d/color/ColorWithAlternatives.java new file mode 100644 index 0000000..e380c62 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/java2d/color/ColorWithAlternatives.java @@ -0,0 +1,220 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ColorWithAlternatives.java 1804124 2017-08-04 14:13:54Z ssteiner $ */ + +package org.apache.xmlgraphics.java2d.color; + +import java.awt.Color; +import java.awt.color.ColorSpace; +import java.util.Arrays; + +/** + * Extended {@link Color} class allowing to specify a prioritized list of alternative colors. + * The alternative colors shall be the ones that are preferred if an output format supports them. + * This is normally used for passing device-specific colors through to the output format. + *

    + * This class only adds a single reference to a color array which should not increase memory + * consumption by much if no alternative colors are specified. + *

    + * Important: Due to a flaw in {@link Color#equals(Object)}, the equals() + * method should not be used to compare two colors, especially when used to update the current + * color for some output format. {@link Color} only takes the sRGB values into account but not + * more the advanced facets of this class. Use {@link ColorUtil#isSameColor(Color, Color)} for + * such a check. + */ +public class ColorWithAlternatives extends Color { + + private static final long serialVersionUID = -6125884937776779150L; + + private Color[] alternativeColors; + + /** + * Constructor for RGBA colors. + * @param r the red component + * @param g the green component + * @param b the blue component + * @param a the alpha component + * @param alternativeColors the prioritized list of alternative colors. + * @see Color#Color(float, float, float, float) + */ + public ColorWithAlternatives(float r, float g, float b, float a, Color[] alternativeColors) { + super(r, g, b, a); + initAlternativeColors(alternativeColors); + } + + /** + * Constructor for RGB colors. + * @param r the red component + * @param g the green component + * @param b the blue component + * @param alternativeColors the prioritized list of alternative colors. + * @see Color#Color(float, float, float) + */ + public ColorWithAlternatives(float r, float g, float b, Color[] alternativeColors) { + super(r, g, b); + initAlternativeColors(alternativeColors); + } + + /** + * Constructor for RGBA colors. + * @param rgba the combined RGBA value + * @param hasalpha true if the alpha bits are valid, false otherwise + * @param alternativeColors the prioritized list of alternative colors. + * @see Color#Color(int, boolean) + */ + public ColorWithAlternatives(int rgba, boolean hasalpha, Color[] alternativeColors) { + super(rgba, hasalpha); + initAlternativeColors(alternativeColors); + } + + /** + * Constructor for RGBA colors. + * @param r the red component + * @param g the green component + * @param b the blue component + * @param a the alpha component + * @param alternativeColors the prioritized list of alternative colors. + * @see Color#Color(int, int, int, int) + */ + public ColorWithAlternatives(int r, int g, int b, int a, Color[] alternativeColors) { + super(r, g, b, a); + initAlternativeColors(alternativeColors); + } + + /** + * Constructor for RGB colors. + * @param r the red component + * @param g the green component + * @param b the blue component + * @param alternativeColors the prioritized list of alternative colors. + * @see Color#Color(int, int, int) + */ + public ColorWithAlternatives(int r, int g, int b, Color[] alternativeColors) { + super(r, g, b); + initAlternativeColors(alternativeColors); + } + + /** + * Constructor for RGB colors. + * @param rgb the combined RGB components + * @param alternativeColors the prioritized list of alternative colors. + * @see Color#Color(int) + */ + public ColorWithAlternatives(int rgb, Color[] alternativeColors) { + super(rgb); + initAlternativeColors(alternativeColors); + } + + /** + * Constructor for colors with an arbitrary color space. + * @param cspace the color space + * @param components the color components + * @param alpha the alpha component + * @param alternativeColors the prioritized list of alternative colors. + * @see Color#Color(ColorSpace, float[], float) + */ + public ColorWithAlternatives(ColorSpace cspace, float[] components, float alpha, + Color[] alternativeColors) { + super(cspace, components, alpha); + initAlternativeColors(alternativeColors); + } + + private void initAlternativeColors(Color[] colors) { + if (colors != null) { + //Colors are immutable but array are not, so copy + this.alternativeColors = new Color[colors.length]; + System.arraycopy(colors, 0, this.alternativeColors, 0, colors.length); + } + } + + /** + * Returns the list of alternative colors. An empty array will be returned if no alternative + * colors are available. + * @return the list of alternative colors + */ + public Color[] getAlternativeColors() { + if (this.alternativeColors != null) { + Color[] cols = new Color[this.alternativeColors.length]; + System.arraycopy(this.alternativeColors, 0, cols, 0, this.alternativeColors.length); + return cols; + } else { + return new Color[0]; + } + } + + /** + * Indicates whether alternative colors are available. + * @return true if alternative colors are available. + */ + public boolean hasAlternativeColors() { + return this.alternativeColors != null && this.alternativeColors.length > 0; + } + + /** + * Indicates whether another instance has the same alternative colors. + * @param col the color to compare the alternatives to + * @return true if the same alternative colors are present + */ + public boolean hasSameAlternativeColors(ColorWithAlternatives col) { + if (!hasAlternativeColors()) { + return !col.hasAlternativeColors(); + } + // this.hasAlternativeColors() + if (!col.hasAlternativeColors()) { + return false; + } + // this.hasAlternativeColors() && col.hasAlternativeColors() + Color[] alt1 = getAlternativeColors(); + Color[] alt2 = col.getAlternativeColors(); + if (alt1.length != alt2.length) { + return false; + } + for (int i = 0, c = alt1.length; i < c; i++) { + Color c1 = alt1[i]; + Color c2 = alt2[i]; + if (!ColorUtil.isSameColor(c1, c2)) { + return false; + } + } + return true; + } + + /** + * Returns the first alternative color found with the given color space type. + * @param colorSpaceType the color space type ({@link ColorSpace}.TYPE_*). + * @return the requested alternative color or null, if no match was found + */ + public Color getFirstAlternativeOfType(int colorSpaceType) { + if (hasAlternativeColors()) { + for (Color alternativeColor : this.alternativeColors) { + if (alternativeColor.getColorSpace().getType() == colorSpaceType) { + return alternativeColor; + } + } + } + return null; + } + + public int hashCode() { + int hash = super.hashCode(); + if (alternativeColors != null) { + hash = 37 * hash + Arrays.hashCode(alternativeColors); + } + return hash; + } +} diff --git a/src/main/java/org/apache/xmlgraphics/java2d/color/DefaultColorConverter.java b/src/main/java/org/apache/xmlgraphics/java2d/color/DefaultColorConverter.java new file mode 100644 index 0000000..36df77b --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/java2d/color/DefaultColorConverter.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DefaultColorConverter.java 1345683 2012-06-03 14:50:33Z gadams $ */ + +package org.apache.xmlgraphics.java2d.color; + +import java.awt.Color; + +/** + * A default implementation that does not apply any conversion + */ +public final class DefaultColorConverter implements ColorConverter { + + /** + * private constructor to support singleton pattern + */ + private static final DefaultColorConverter SINGLETON = new DefaultColorConverter(); + + private DefaultColorConverter() { + } + + /** + * static factory + * + * @return singleton instance of DefaultColorConverter + */ + public static DefaultColorConverter getInstance() { + return SINGLETON; + } + + /** {@inheritDoc} */ + public Color convert(Color color) { + return color; + } +} diff --git a/src/main/java/org/apache/xmlgraphics/java2d/color/DeviceCMYKColorSpace.java b/src/main/java/org/apache/xmlgraphics/java2d/color/DeviceCMYKColorSpace.java new file mode 100644 index 0000000..d4e699b --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/java2d/color/DeviceCMYKColorSpace.java @@ -0,0 +1,117 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DeviceCMYKColorSpace.java 1400596 2012-10-21 08:49:02Z gadams $ */ + +package org.apache.xmlgraphics.java2d.color; + +import java.awt.Color; + +/** + * This class represents an uncalibrated CMYK color space. + */ +public class DeviceCMYKColorSpace extends AbstractDeviceSpecificColorSpace + implements ColorSpaceOrigin { + + private static final long serialVersionUID = 2925508946083542974L; + + /** The name for the uncalibrated CMYK pseudo-profile */ + public static final String PSEUDO_PROFILE_NAME = "#CMYK"; + + /** + * Constructs an uncalibrated CMYK ColorSpace object with + * {@link java.awt.color.ColorSpace#TYPE_CMYK} and 4 components. + * @see java.awt.color.ColorSpace#ColorSpace(int, int) + */ + public DeviceCMYKColorSpace() { + super(TYPE_CMYK, 4); + } + + /** + * Returns an instance of an uncalibrated CMYK color space. + * @return CMYKColorSpace the requested color space object + * @deprecated Use {@link ColorSpaces#getDeviceCMYKColorSpace()} instead. + */ + @Deprecated + public static DeviceCMYKColorSpace getInstance() { + return ColorSpaces.getDeviceCMYKColorSpace(); + } + + /** {@inheritDoc} */ + @Override + public float[] toRGB(float[] colorvalue) { + return new float [] { + (1 - colorvalue[0]) * (1 - colorvalue[3]), + (1 - colorvalue[1]) * (1 - colorvalue[3]), + (1 - colorvalue[2]) * (1 - colorvalue[3])}; + } + + /** {@inheritDoc} */ + @Override + public float[] fromRGB(float[] rgbvalue) { + assert rgbvalue.length == 3; + //Note: this is an arbitrary conversion, not a color-managed one! + float r = rgbvalue[0]; + float g = rgbvalue[1]; + float b = rgbvalue[2]; + if (r == g && r == b) { + return new float[] {0, 0, 0, 1 - r}; + } else { + float c = 1 - r; + float m = 1 - g; + float y = 1 - b; + float k = Math.min(c, Math.min(m, y)); + return new float[] {c, m, y, k}; + } + } + + /** {@inheritDoc} */ + @Override + public float[] toCIEXYZ(float[] colorvalue) { + throw new UnsupportedOperationException("NYI"); + } + + /** {@inheritDoc} */ + @Override + public float[] fromCIEXYZ(float[] colorvalue) { + throw new UnsupportedOperationException("NYI"); + } + + /** + * Creates a color instance representing a device-specific CMYK color. An sRGB value + * is calculated from the CMYK colors but it may not correctly represent the given CMYK + * values. + * @param cmykComponents the CMYK components + * @return the device-specific color + */ + public static Color createCMYKColor(float[] cmykComponents) { + DeviceCMYKColorSpace cmykCs = ColorSpaces.getDeviceCMYKColorSpace(); + Color cmykColor = new ColorWithAlternatives(cmykCs, cmykComponents, 1.0f, null); + return cmykColor; + } + + /** {@inheritDoc} */ + public String getProfileName() { + return PSEUDO_PROFILE_NAME; + } + + /** {@inheritDoc} */ + public String getProfileURI() { + return null; //No URI + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/java2d/color/GrayScaleColorConverter.java b/src/main/java/org/apache/xmlgraphics/java2d/color/GrayScaleColorConverter.java new file mode 100644 index 0000000..cd47a12 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/java2d/color/GrayScaleColorConverter.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: GrayScaleColorConverter.java 1345683 2012-06-03 14:50:33Z gadams $ */ + +package org.apache.xmlgraphics.java2d.color; + +import java.awt.Color; + +/** + * Converts to grayscale using the standard RED=30%, GREEN=59% and BLUE=11% + * weights (see http://en.wikipedia.org/wiki/Grayscale) + */ +public final class GrayScaleColorConverter implements ColorConverter { + + private static final int RED_WEIGHT = 77; + private static final int GREEN_WEIGTH = 150; + private static final int BLUE_WEIGHT = 28; + + private static final GrayScaleColorConverter SINGLETON = new GrayScaleColorConverter(); + + private GrayScaleColorConverter() { + } + + /** + * Returns a singleton instance. + * + * @return singleton instance of GrayScaleColorConverter + */ + public static GrayScaleColorConverter getInstance() { + return SINGLETON; + } + + /** + * The color is converted to CMYK with just the K component. + * {@inheritDoc} + */ + public Color convert(Color color) { + + float kValue = (RED_WEIGHT * color.getRed() + + GREEN_WEIGTH * color.getGreen() + + BLUE_WEIGHT * color.getBlue()) / 255.0f / 255.0f; + + return ColorUtil.toCMYKGrayColor(kValue); + } +} diff --git a/src/main/java/org/apache/xmlgraphics/java2d/color/ICCColorSpaceWithIntent.java b/src/main/java/org/apache/xmlgraphics/java2d/color/ICCColorSpaceWithIntent.java new file mode 100644 index 0000000..f78f9e9 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/java2d/color/ICCColorSpaceWithIntent.java @@ -0,0 +1,143 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ICCColorSpaceWithIntent.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.java2d.color; + +import java.awt.color.ColorSpace; +import java.awt.color.ICC_ColorSpace; +import java.awt.color.ICC_Profile; + +/** + * This class extends the ICCColorSpace class by providing + * convenience methods to convert to sRGB using various + * methods, forcing a given intent, such as perceptual or + * relative colorimetric. It also additionally holds the name + * and source URI of the color profile. + */ +public class ICCColorSpaceWithIntent extends ICC_ColorSpace implements ColorSpaceOrigin { + + private static final long serialVersionUID = -3338065900662625221L; + + static final ColorSpace SRGB = ColorSpace.getInstance(ColorSpace.CS_sRGB); + + private RenderingIntent intent; + private String profileName; + private String profileURI; + + /** + * Creates a new ICC-based color space. + * @param p the color profile + * @param intent the overriding rendering intent (use {@link RenderingIntent#AUTO} + * to preserve the profile's) + * @param profileName the color profile name + * @param profileURI the source URI of the color profile + */ + public ICCColorSpaceWithIntent(ICC_Profile p, RenderingIntent intent, + String profileName, String profileURI) { + super(p); + + this.intent = intent; + + /** + * Apply the requested intent into the profile + */ + if (intent != RenderingIntent.AUTO) { + byte[] hdr = p.getData(ICC_Profile.icSigHead); + hdr[ICC_Profile.icHdrRenderingIntent] = (byte)intent.getIntegerValue(); + } + + this.profileName = profileName; + this.profileURI = profileURI; + } + + /** + * Returns the sRGB value obtained by forcing the + * conversion method to the intent passed to the + * constructor. + * @param values the color values in the local color space + * @return the sRGB values + */ + public float[] intendedToRGB(float[] values) { + switch(intent) { + case ABSOLUTE_COLORIMETRIC: + return absoluteColorimetricToRGB(values); + case PERCEPTUAL: + case AUTO: + return perceptualToRGB(values); + case RELATIVE_COLORIMETRIC: + return relativeColorimetricToRGB(values); + case SATURATION: + return saturationToRGB(values); + default: + throw new RuntimeException("invalid intent:" + intent); + } + } + + /** + * Perceptual conversion is the method implemented by the + * base class's toRGB method + * @param values the color values in the local color space + * @return the sRGB values + */ + private float[] perceptualToRGB(float[] values) { + return toRGB(values); + } + + /** + * Relative colorimetric needs to happen through CIEXYZ + * conversion. + * @param values the color values in the local color space + * @return the sRGB values + */ + private float[] relativeColorimetricToRGB(float[] values) { + float[] ciexyz = toCIEXYZ(values); + return SRGB.fromCIEXYZ(ciexyz); + } + + /** + * Absolute colorimetric. NOT IMPLEMENTED. + * Temporarily returns same as perceptual. + * @param values the color values in the local color space + * @return the sRGB values + */ + private float[] absoluteColorimetricToRGB(float[] values) { + return perceptualToRGB(values); + } + + /** + * Saturation. NOT IMPLEMENTED. Temporarily returns same + * as perceptual. + * @param values the color values in the local color space + * @return the sRGB values + */ + private float[] saturationToRGB(float[] values) { + return perceptualToRGB(values); + } + + /** {@inheritDoc} */ + public String getProfileName() { + return this.profileName; + } + + /** {@inheritDoc} */ + public String getProfileURI() { + return this.profileURI; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/java2d/color/NamedColorSpace.java b/src/main/java/org/apache/xmlgraphics/java2d/color/NamedColorSpace.java new file mode 100644 index 0000000..eea60a1 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/java2d/color/NamedColorSpace.java @@ -0,0 +1,219 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: NamedColorSpace.java 1051421 2010-12-21 08:54:25Z jeremias $ */ + +package org.apache.xmlgraphics.java2d.color; + +import java.awt.Color; +import java.awt.color.ColorSpace; + +/** + * Implements a pseudo color space for a named color which is defined in the CIE XYZ color space. + * At the moment, this color space always returns the fully opaque color regardless of the single + * component value (tint) given to its conversion methods. + */ +public class NamedColorSpace extends ColorSpace implements ColorSpaceOrigin { + + private static final long serialVersionUID = -8957543225908514658L; + + private final String name; + private final float[] xyz; + + private final String profileName; + private final String profileURI; + + /** + * Creates a new named color. + * @param name the color name + * @param xyz the CIE XYZ coordinates (valid values: 0.0f to 1.0f, although + * values slightly larger than 1.0f are common) + */ + public NamedColorSpace(String name, float[] xyz) { + this(name, xyz, null, null); + } + + /** + * Creates a new named color. + * @param name the color name + * @param xyz the CIE XYZ coordinates (valid values: 0.0f to 1.0f, although + * values slightly larger than 1.0f are common) + * @param profileName Optional profile name associated with this color space + * @param profileURI Optional profile URI associated with this color space + */ + public NamedColorSpace(String name, float[] xyz, String profileName, String profileURI) { + super(ColorSpace.TYPE_GRAY, 1); + checkNumComponents(xyz, 3); + if (name == null || name.trim().length() == 0) { + throw new IllegalArgumentException("No name provided for named color space"); + } + this.name = name.trim(); + this.xyz = new float[3]; + System.arraycopy(xyz, 0, this.xyz, 0, 3); + this.profileName = profileName; + this.profileURI = profileURI; + } + + /** + * Creates a new named color. + * @param name the color name + * @param color the color to use when the named color's specific color properties are not + * available. + * @param profileName Optional profile name associated with this color space + * @param profileURI Optional profile URI associated with this color space + */ + public NamedColorSpace(String name, Color color, String profileName, String profileURI) { + this(name, color.getColorSpace().toCIEXYZ(color.getColorComponents(null)), + profileName, profileURI); + } + + /** + * Creates a new named color. + * @param name the color name + * @param color the color to use when the named color's specific color properties are not + * available. + */ + public NamedColorSpace(String name, Color color) { + this(name, color.getColorSpace().toCIEXYZ(color.getColorComponents(null)), null, null); + } + + private void checkNumComponents(float[] colorvalue, int expected) { + if (colorvalue == null) { + throw new NullPointerException("color value may not be null"); + } + if (colorvalue.length != expected) { + throw new IllegalArgumentException("Expected " + expected + + " components, but got " + colorvalue.length); + } + } + + /** + * Returns the color name. + * @return the color name + */ + public String getColorName() { + return this.name; + } + + /** {@inheritDoc} */ + public String getProfileName() { + return this.profileName; + } + + /** {@inheritDoc} */ + public String getProfileURI() { + return this.profileURI; + } + + /** + * Returns the XYZ coordinates of the named color. + * @return the XYZ coordinates of the named color + */ + public float[] getXYZ() { + float[] result = new float[this.xyz.length]; + System.arraycopy(this.xyz, 0, result, 0, this.xyz.length); + return result; + } + + /** + * Returns an sRGB-based color representing the full-tint color defined by this named color + * space. + * @return the sRGB color + */ + public Color getRGBColor() { + float[] comps = toRGB(this.xyz); + return new Color(comps[0], comps[1], comps[2]); + } + + /** {@inheritDoc} */ + public float getMinValue(int component) { + return getMaxValue(component); //same as min, i.e. always 1.0 + } + + /** {@inheritDoc} */ + public float getMaxValue(int component) { + switch (component) { + case 0: + return 1f; + default: + throw new IllegalArgumentException("A named color space only has 1 component!"); + } + } + + /** {@inheritDoc} */ + public String getName(int component) { + switch (component) { + case 0: + return "Tint"; + default: + throw new IllegalArgumentException("A named color space only has 1 component!"); + } + } + + /** {@inheritDoc} */ + public float[] fromCIEXYZ(float[] colorvalue) { + //ignore the given color values as this is a fixed color. + return new float[] {1.0f}; //Return value for full tint + } + + /** {@inheritDoc} */ + public float[] fromRGB(float[] rgbvalue) { + //ignore the given color values as this is a fixed color. + return new float[] {1.0f}; //Return value for full tint + } + + /** {@inheritDoc} */ + public float[] toCIEXYZ(float[] colorvalue) { + float[] ret = new float[3]; + System.arraycopy(this.xyz, 0, ret, 0, this.xyz.length); + return ret; + } + + /** {@inheritDoc} */ + public float[] toRGB(float[] colorvalue) { + ColorSpace sRGB = ColorSpace.getInstance(ColorSpace.CS_sRGB); + return sRGB.fromCIEXYZ(this.xyz); + } + + /** {@inheritDoc} */ + public boolean equals(Object obj) { + if (!(obj instanceof NamedColorSpace)) { + return false; + } + NamedColorSpace other = (NamedColorSpace)obj; + if (!this.name.equals(other.name)) { + return false; + } + for (int i = 0, c = this.xyz.length; i < c; i++) { + if (this.xyz[i] != other.xyz[i]) { + return false; + } + } + return true; + } + + /** {@inheritDoc} */ + public int hashCode() { + return (this.profileName + name).hashCode(); + } + + /** {@inheritDoc} */ + public String toString() { + return "Named Color Space: " + getColorName(); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/java2d/color/RenderingIntent.java b/src/main/java/org/apache/xmlgraphics/java2d/color/RenderingIntent.java new file mode 100644 index 0000000..f1e073e --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/java2d/color/RenderingIntent.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: RenderingIntent.java 1051421 2010-12-21 08:54:25Z jeremias $ */ + +package org.apache.xmlgraphics.java2d.color; + +import java.awt.color.ICC_Profile; + +/** + * Enumeration for rendering intents. + */ +public enum RenderingIntent { + + /** Perceptual rendering intent. Typical use: scanned images. */ + PERCEPTUAL(ICC_Profile.icPerceptual), + /** Relative colorimetric rendering intent. Typical use: vector graphics. */ + RELATIVE_COLORIMETRIC(ICC_Profile.icRelativeColorimetric), + /** Absolute colorimetric rendering intent. Typical use: logos and solid colors. */ + ABSOLUTE_COLORIMETRIC(ICC_Profile.icAbsoluteColorimetric), + /** Saturation rendering intent. Typical use: business graphics. */ + SATURATION(ICC_Profile.icSaturation), + /** Automatic rendering intent. The color profile's intent isn't overridden. */ + AUTO(4); + + private int intValue; + + private RenderingIntent(int value) { + this.intValue = value; + } + + /** + * Returns an integer value identifying the rendering intent. This is the same value defined + * by the ICC specification (0..3) plus one for "auto" (4). (See also {@link ICC_Profile}.ic*) + * @return the integer value + */ + public int getIntegerValue() { + return this.intValue; + } + + /** + * Returns the enum value for the given integer rendering intent (as defined by the ICC + * specification). + * @param value the rendering intent as ICC value + * @return the matching enum + */ + public static RenderingIntent fromICCValue(int value) { + switch (value) { + case ICC_Profile.icPerceptual: return PERCEPTUAL; + case ICC_Profile.icRelativeColorimetric: return RELATIVE_COLORIMETRIC; + case ICC_Profile.icAbsoluteColorimetric: return ABSOLUTE_COLORIMETRIC; + case ICC_Profile.icSaturation: return SATURATION; + default: + throw new IllegalArgumentException("Invalid value for rendering intent: " + value); + } + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/java2d/color/package.html b/src/main/java/org/apache/xmlgraphics/java2d/color/package.html new file mode 100644 index 0000000..e771c1e --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/java2d/color/package.html @@ -0,0 +1,10 @@ + + + Extended color infrastructure for Java2D + + +

    + Provides additional color infrastructure so SVG and XSL-FO implementations can store additional color information and do things like specialized color conversions. +

    + + diff --git a/src/main/java/org/apache/xmlgraphics/java2d/color/profile/ColorProfileUtil.java b/src/main/java/org/apache/xmlgraphics/java2d/color/profile/ColorProfileUtil.java new file mode 100644 index 0000000..377d9af --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/java2d/color/profile/ColorProfileUtil.java @@ -0,0 +1,148 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ColorProfileUtil.java 1400525 2012-10-20 22:03:56Z lbernardo $ */ + +package org.apache.xmlgraphics.java2d.color.profile; + +import java.awt.color.ColorSpace; +import java.awt.color.ICC_Profile; +import java.awt.color.ICC_ProfileRGB; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.util.Arrays; + +// CSOFF: MethodName + +/** + * Helper methods for handling color profiles. + */ +public final class ColorProfileUtil { + + private ColorProfileUtil() { + } + + /** + * Returns the profile description of an ICC profile + * @param profile the profile + * @return the description + */ + public static String getICCProfileDescription(ICC_Profile profile) { + byte[] data = profile.getData(ICC_Profile.icSigProfileDescriptionTag); + if (data == null) { + return null; + } else { + //Info on the data format: http://www.color.org/ICC-1_1998-09.PDF + int length = (data[8] << 3 * 8) | (data[9] << 2 * 8) | (data[10] << 8) | data[11]; + length--; //Remove trailing NUL character + try { + return new String(data, 12, length, "US-ASCII"); + } catch (UnsupportedEncodingException e) { + throw new UnsupportedOperationException("Incompatible VM"); + } + } + } + + /** + * Indicates whether a given color profile is identical to the default sRGB profile + * provided by the Java class library. + * @param profile the color profile to check + * @return true if it is the default sRGB profile + */ + public static boolean isDefaultsRGB(ICC_Profile profile) { + if (!(profile instanceof ICC_ProfileRGB)) { + return false; + } + // not sure what the best way to compare two profiles is, but comparing instances is not the right way + ICC_Profile sRGBProfile = ICC_Profile.getInstance(ColorSpace.CS_sRGB); + if (profile.getProfileClass() != sRGBProfile.getProfileClass()) { + return false; + } + if (profile.getMajorVersion() != sRGBProfile.getMajorVersion()) { + return false; + } + if (profile.getMinorVersion() != sRGBProfile.getMinorVersion()) { + return false; + } + if (!Arrays.equals(profile.getData(), sRGBProfile.getData())) { + return false; + } + return true; + } + + /** + * Proxy method for {@link ICC_Profile#getInstance(byte[])} + * that properly synchronizes the call to avoid a potential race condition. + * @param data the specified ICC Profile data + * @return an {@link ICC_Profile} instance corresponding to the data in the + * specified byte array + */ + public static ICC_Profile getICC_Profile(byte[] data) { + synchronized (ICC_Profile.class) { + return ICC_Profile.getInstance(data); + } + } + + /** + * Proxy method for {@link ICC_Profile#getInstance(int)} + * that properly synchronizes the call to avoid a potential race condition. + * @param colorSpace the type of color space to create a profile for. The specified type is + * one of the color space constants defined in the {@link ColorSpace} + * class. + * @return an {@link ICC_Profile} instance corresponding to the specified {@code ColorSpace} + * @throws IllegalArgumentException if {@code colorSpace} is not one of the predefined types + */ + public static ICC_Profile getICC_Profile(int colorSpace) { + synchronized (ICC_Profile.class) { + return ICC_Profile.getInstance(colorSpace); + } + } + + /** + * Proxy method for {@link ICC_Profile#getInstance(java.io.InputStream)} + * that properly synchronizes the call to avoid a potential race condition. + * @param in the input stream from which to read the profile data + * @return an {@link ICC_Profile} instance corresponding to the data in the + * specified {@link InputStream} + * @throws IOException if an I/O error occurs while reading the stream + * @throws IllegalArgumentException if the stream does not contain valid ICC Profile data + */ + public static ICC_Profile getICC_Profile(InputStream in) throws IOException { + synchronized (ICC_Profile.class) { + return ICC_Profile.getInstance(in); + } + } + + /** + * Proxy method for {@link ICC_Profile#getInstance(java.lang.String)} + * that properly synchronizes the call to avoid a potential race condition. + * @param fileName the name of the file that contains the profile data + * @return an {@link ICC_Profile} instance corresponding to the data in the specified file + * @throws IOException if the file cannot be opened, or an I/O error occurs while reading + * the stream + * @throws IllegalArgumentException if the stream does not contain valid ICC Profile data + * @throws SecurityException if a security manager is installed and it does not permit read + * access to the given file. + */ + public static ICC_Profile getICC_Profile(String fileName) throws IOException { + synchronized (ICC_Profile.class) { + return ICC_Profile.getInstance(fileName); + } + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/java2d/color/profile/NamedColorProfile.java b/src/main/java/org/apache/xmlgraphics/java2d/color/profile/NamedColorProfile.java new file mode 100644 index 0000000..3a927b3 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/java2d/color/profile/NamedColorProfile.java @@ -0,0 +1,110 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: NamedColorProfile.java 1804124 2017-08-04 14:13:54Z ssteiner $ */ + +package org.apache.xmlgraphics.java2d.color.profile; + +import org.apache.xmlgraphics.java2d.color.NamedColorSpace; +import org.apache.xmlgraphics.java2d.color.RenderingIntent; + +/** + * Simplified in-memory representation of an ICC named color profile. + */ +public class NamedColorProfile { + + private String profileName; + private String copyright; + private NamedColorSpace[] namedColors; + private RenderingIntent renderingIntent = RenderingIntent.PERCEPTUAL; + + /** + * Creates a new named color profile. + * @param profileName the profile name + * @param copyright the copyright + * @param namedColors the array of named colors + * @param intent the rendering intent + */ + public NamedColorProfile(String profileName, String copyright, NamedColorSpace[] namedColors, + RenderingIntent intent) { + this.profileName = profileName; + this.copyright = copyright; + this.namedColors = namedColors; + this.renderingIntent = intent; + } + + /** + * Returns the color profile's rendering intent. + * @return the rendering intent + * (See {@link java.awt.color.ICC_Profile}.ic*) + */ + public RenderingIntent getRenderingIntent() { + return this.renderingIntent; + } + + /** + * Returns the array of named colors. + * @return the array of named colors + */ + public NamedColorSpace[] getNamedColors() { + NamedColorSpace[] copy = new NamedColorSpace[this.namedColors.length]; + System.arraycopy(this.namedColors, 0, copy, 0, this.namedColors.length); + return copy; + } + + /** + * Returns a named color. + * @param name the color name + * @return the named color (or null if it is not available) + */ + public NamedColorSpace getNamedColor(String name) { + if (this.namedColors != null) { + for (NamedColorSpace namedColor : this.namedColors) { + if (namedColor.getColorName().equals(name)) { + return namedColor; + } + } + } + return null; + } + + /** + * Returns the profile name. + * @return the profile name + */ + public String getProfileName() { + return this.profileName; + } + + /** + * Returns the profile copyright. + * @return the profile copyright + */ + public String getCopyright() { + return this.copyright; + } + + /** {@inheritDoc} */ + @Override + public String toString() { + StringBuffer sb = new StringBuffer("Named color profile: "); + sb.append(getProfileName()); + sb.append(", ").append(namedColors.length).append(" colors"); + return sb.toString(); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/java2d/color/profile/NamedColorProfileParser.java b/src/main/java/org/apache/xmlgraphics/java2d/color/profile/NamedColorProfileParser.java new file mode 100644 index 0000000..5de35f7 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/java2d/color/profile/NamedColorProfileParser.java @@ -0,0 +1,196 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: NamedColorProfileParser.java 1681108 2015-05-22 13:26:12Z ssteiner $ */ + +package org.apache.xmlgraphics.java2d.color.profile; + +import java.awt.color.ColorSpace; +import java.awt.color.ICC_Profile; +import java.io.ByteArrayInputStream; +import java.io.DataInput; +import java.io.DataInputStream; +import java.io.IOException; + +import org.apache.xmlgraphics.java2d.color.CIELabColorSpace; +import org.apache.xmlgraphics.java2d.color.ColorSpaces; +import org.apache.xmlgraphics.java2d.color.NamedColorSpace; +import org.apache.xmlgraphics.java2d.color.RenderingIntent; + +/** + * This class is a parser for ICC named color profiles. It uses Java's {@link ICC_Profile} class + * for parsing the basic structure but adds functionality to parse certain profile tags. + */ +public class NamedColorProfileParser { + + private static final int MLUC = 0x6D6C7563; //'mluc' + private static final int NCL2 = 0x6E636C32; //'ncl2' + + /** + * Indicates whether the profile is a named color profile. + * @param profile the color profile + * @return true if the profile is a named color profile, false otherwise + */ + public static boolean isNamedColorProfile(ICC_Profile profile) { + return profile.getProfileClass() == ICC_Profile.CLASS_NAMEDCOLOR; + } + + /** + * Parses a named color profile (NCP). + * @param profile the profile to analyze + * @param profileName Optional profile name associated with this color profile + * @param profileURI Optional profile URI associated with this color profile + * @return an object representing the parsed NCP + * @throws IOException if an I/O error occurs + */ + public NamedColorProfile parseProfile(ICC_Profile profile, + String profileName, String profileURI) throws IOException { + if (!isNamedColorProfile(profile)) { + throw new IllegalArgumentException("Given profile is not a named color profile (NCP)"); + } + String profileDescription = getProfileDescription(profile); + String copyright = getCopyright(profile); + RenderingIntent intent = getRenderingIntent(profile); + NamedColorSpace[] ncs = readNamedColors(profile, profileName, profileURI); + return new NamedColorProfile(profileDescription, copyright, ncs, intent); + } + + /** + * Parses a named color profile (NCP). + * @param profile the profile to analyze + * @return an object representing the parsed NCP + * @throws IOException if an I/O error occurs + */ + public NamedColorProfile parseProfile(ICC_Profile profile) throws IOException { + return parseProfile(profile, null, null); + } + + private String getProfileDescription(ICC_Profile profile) throws IOException { + byte[] tag = profile.getData(ICC_Profile.icSigProfileDescriptionTag); + return readSimpleString(tag); + } + + private String getCopyright(ICC_Profile profile) throws IOException { + byte[] tag = profile.getData(ICC_Profile.icSigCopyrightTag); + return readSimpleString(tag); + } + + private RenderingIntent getRenderingIntent(ICC_Profile profile) throws IOException { + byte[] hdr = profile.getData(ICC_Profile.icSigHead); + int value = hdr[ICC_Profile.icHdrRenderingIntent]; + return RenderingIntent.fromICCValue(value); + } + + private NamedColorSpace[] readNamedColors(ICC_Profile profile, + String profileName, String profileURI) throws IOException { + byte[] tag = profile.getData(ICC_Profile.icSigNamedColor2Tag); + DataInput din = new DataInputStream(new ByteArrayInputStream(tag)); + int sig = din.readInt(); + if (sig != NCL2) { + throw new UnsupportedOperationException("Unsupported structure type: " + + toSignatureString(sig) + ". Expected " + toSignatureString(NCL2)); + } + din.skipBytes(8); + int numColors = din.readInt(); + NamedColorSpace[] result = new NamedColorSpace[numColors]; + int numDeviceCoord = din.readInt(); + String prefix = readAscii(din, 32); + String suffix = readAscii(din, 32); + for (int i = 0; i < numColors; i++) { + String name = prefix + readAscii(din, 32) + suffix; + int[] pcs = readUInt16Array(din, 3); + float[] colorvalue = new float[3]; + for (int j = 0; j < pcs.length; j++) { + colorvalue[j] = ((float)pcs[j]) / 0x8000; + } + + //device coordinates are ignored for now + /*int[] deviceCoord =*/ readUInt16Array(din, numDeviceCoord); + + switch (profile.getPCSType()) { + case ColorSpace.TYPE_XYZ: + result[i] = new NamedColorSpace(name, colorvalue, profileName, profileURI); + break; + case ColorSpace.TYPE_Lab: + //Not sure if this always D50 here, + //but the illuminant in the header is fixed to D50. + CIELabColorSpace labCS = ColorSpaces.getCIELabColorSpaceD50(); + result[i] = new NamedColorSpace(name, labCS.toColor(colorvalue, 1.0f), + profileName, profileURI); + break; + default: + throw new UnsupportedOperationException( + "PCS type is not supported: " + profile.getPCSType()); + } + } + return result; + } + + private int[] readUInt16Array(DataInput din, int count) throws IOException { + if (count == 0) { + return new int[0]; + } + int[] result = new int[count]; + for (int i = 0; i < count; i++) { + int v = din.readUnsignedShort(); + result[i] = v; + } + return result; + } + + private String readAscii(DataInput in, int maxLength) throws IOException { + byte[] data = new byte[maxLength]; + in.readFully(data); + String result = new String(data, "US-ASCII"); + int idx = result.indexOf('\0'); + if (idx >= 0) { + result = result.substring(0, idx); + } + return result; + } + + private String readSimpleString(byte[] tag) throws IOException { + DataInput din = new DataInputStream(new ByteArrayInputStream(tag)); + int sig = din.readInt(); + if (sig == MLUC) { + return readMLUC(din); + } else { + return null; //Unsupported tag structure type + } + } + + private String readMLUC(DataInput din) throws IOException { + din.skipBytes(16); + int firstLength = din.readInt(); + int firstOffset = din.readInt(); + int offset = 28; + din.skipBytes(firstOffset - offset); + byte[] utf16 = new byte[firstLength]; + din.readFully(utf16); + return new String(utf16, "UTF-16BE"); + } + + private String toSignatureString(int sig) { + StringBuffer sb = new StringBuffer(); + sb.append((char)(sig >> 24 & 0xFF)); + sb.append((char)(sig >> 16 & 0xFF)); + sb.append((char)(sig >> 8 & 0xFF)); + sb.append((char)(sig & 0xFF)); + return sb.toString(); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/java2d/color/profile/package.html b/src/main/java/org/apache/xmlgraphics/java2d/color/profile/package.html new file mode 100644 index 0000000..81c5c91 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/java2d/color/profile/package.html @@ -0,0 +1,10 @@ + + + Additional color around the topic of color profiles + + +

    + Provides classes around the topic of color profiles, like support for ICC named color profiles. +

    + + diff --git a/src/main/java/org/apache/xmlgraphics/java2d/package.html b/src/main/java/org/apache/xmlgraphics/java2d/package.html new file mode 100644 index 0000000..fceacdc --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/java2d/package.html @@ -0,0 +1,12 @@ + + + Java2D utilities and extensions + + +

    + Provides convenience base and utility classes for subclassing the + java.awt.Graphics2D class in order to translate + Java 2D primitives into another graphic format. +

    + + diff --git a/src/main/java/org/apache/xmlgraphics/java2d/ps/AbstractPSDocumentGraphics2D.java b/src/main/java/org/apache/xmlgraphics/java2d/ps/AbstractPSDocumentGraphics2D.java new file mode 100644 index 0000000..18a2674 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/java2d/ps/AbstractPSDocumentGraphics2D.java @@ -0,0 +1,291 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: AbstractPSDocumentGraphics2D.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.java2d.ps; + +import java.awt.Color; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.io.IOException; +import java.io.OutputStream; + +import org.apache.xmlgraphics.ps.DSCConstants; +import org.apache.xmlgraphics.ps.PSGenerator; +import org.apache.xmlgraphics.ps.PSProcSets; + +/** + * This class is a wrapper for the PSGraphics2D that + * is used to create a full document around the PostScript rendering from + * PSGraphics2D. + * + * @version $Id: AbstractPSDocumentGraphics2D.java 1732018 2016-02-24 04:51:06Z gadams $ + * @see org.apache.xmlgraphics.java2d.ps.PSGraphics2D + * + * Originally authored by Keiron Liddle. + */ +public abstract class AbstractPSDocumentGraphics2D extends PSGraphics2D { + + protected static final Integer ZERO = 0; + + protected int width; + protected int height; + + protected float viewportWidth; + protected float viewportHeight; + + protected int pagecount; + protected boolean pagePending; + + protected Shape initialClip; + protected AffineTransform initialTransform; + + + /** + * Create a new AbstractPSDocumentGraphics2D. + * This is used to create a new PostScript document, the height, + * width and output stream can be setup later. + * For use by the transcoder which needs font information + * for the bridge before the document size is known. + * The resulting document is written to the stream after rendering. + * + * @param textAsShapes set this to true so that text will be rendered + * using curves and not the font. + */ + AbstractPSDocumentGraphics2D(boolean textAsShapes) { + super(textAsShapes); + } + + /** + * Setup the document. + * @param stream the output stream to write the document + * @param width the width of the page + * @param height the height of the page + * @throws IOException an io exception if there is a problem + * writing to the output stream + */ + public void setupDocument(OutputStream stream, int width, int height) throws IOException { + this.width = width; + this.height = height; + this.pagecount = 0; + this.pagePending = false; + + //Setup for PostScript generation + setPSGenerator(new PSGenerator(stream)); + + writeFileHeader(); + } + + /** + * Writes the file header. + * @throws IOException if an I/O error occurs + */ + protected abstract void writeFileHeader() throws IOException; + + /** + * Create a new AbstractPSDocumentGraphics2D. + * This is used to create a new PostScript document of the given height + * and width. + * The resulting document is written to the stream after rendering. + * + * @param textAsShapes set this to true so that text will be rendered + * using curves and not the font. + * @param stream the stream that the final document should be written to. + * @param width the width of the document + * @param height the height of the document + * @throws IOException an io exception if there is a problem + * writing to the output stream + */ + public AbstractPSDocumentGraphics2D(boolean textAsShapes, OutputStream stream, + int width, int height) throws IOException { + this(textAsShapes); + setupDocument(stream, width, height); + } + + /** + * Set the dimensions of the document that will be drawn. + * This is useful if the dimensions of the document are different + * from the PostScript document that is to be created. + * The result is scaled so that the document fits correctly inside the + * PostScript document. + * @param w the width of the page + * @param h the height of the page + * @throws IOException in case of an I/O problem + */ + public void setViewportDimension(float w, float h) throws IOException { + this.viewportWidth = w; + this.viewportHeight = h; + /* + if (w != this.width || h != this.height) { + gen.concatMatrix(width / w, 0, 0, height / h, 0, 0); + }*/ + } + + /** + * Set the background of the PostScript document. + * This is used to set the background for the PostScript document + * Rather than leaving it as the default white. + * @param col the background colour to fill + */ + public void setBackgroundColor(Color col) { + /**(todo) Implement this */ + /* + Color c = col; + PDFColor currentColour = new PDFColor(c.getRed(), c.getGreen(), c.getBlue()); + currentStream.write("q\n"); + currentStream.write(currentColour.getColorSpaceOut(true)); + + currentStream.write("0 0 " + width + " " + height + " re\n"); + + currentStream.write("f\n"); + currentStream.write("Q\n"); + */ + } + + /** + * Returns the number of pages generated so far. + * @return the number of pages + */ + public int getPageCount() { + return this.pagecount; + } + + /** + * Closes the current page and prepares to start a new one. + * @throws IOException if an I/O error occurs + */ + public void nextPage() throws IOException { + closePage(); + } + + /** + * Closes the current page. + * @throws IOException if an I/O error occurs + */ + protected void closePage() throws IOException { + if (!this.pagePending) { + return; //ignore + } + //Finish page + writePageTrailer(); + this.pagePending = false; + } + + /** + * Writes the page header for a page. + * @throws IOException In case an I/O error occurs + */ + protected abstract void writePageHeader() throws IOException; + + /** + * Writes the page trailer for a page. + * @throws IOException In case an I/O error occurs + */ + protected abstract void writePageTrailer() throws IOException; + + /** + * Writes the ProcSets ending up in the prolog to the PostScript file. Override to add your + * own ProcSets if so desired. + * @throws IOException In case an I/O error occurs + */ + protected void writeProcSets() throws IOException { + PSProcSets.writeStdProcSet(gen); + PSProcSets.writeEPSProcSet(gen); + } + + /** {@inheritDoc} */ + public void preparePainting() { + if (this.pagePending) { + return; + } + try { + startPage(); + } catch (IOException ioe) { + handleIOException(ioe); + } + } + + /** + * Starts a new page. + * @throws IOException if an I/O error occurs + */ + protected void startPage() throws IOException { + if (this.pagePending) { + throw new IllegalStateException("Close page first before starting another"); + } + //Start page + this.pagecount++; + + if (this.initialTransform == null) { + //Save initial transformation matrix + this.initialTransform = getTransform(); + this.initialClip = getClip(); + } else { + //Reset transformation matrix + setTransform(this.initialTransform); + setClip(this.initialClip); + } + + writePageHeader(); + AffineTransform at; + if ((this.viewportWidth != this.width + || this.viewportHeight != this.height) + && (this.viewportWidth > 0) && (this.viewportHeight > 0)) { + at = new AffineTransform(this.width / this.viewportWidth, 0, + 0, -1 * (this.height / this.viewportHeight), + 0, this.height); + } else { + at = new AffineTransform(1, 0, 0, -1, 0, this.height); + } + // Do not use concatMatrix, since it alters PSGenerator current state + //gen.concatMatrix(at); + gen.writeln(gen.formatMatrix(at) + " " + gen.mapCommand("concat")); + gen.writeDSCComment(DSCConstants.END_PAGE_SETUP); + this.pagePending = true; + } + + /** + * The rendering process has finished. + * This should be called after the rendering has completed as there is + * no other indication it is complete. + * This will then write the results to the output stream. + * @throws IOException an io exception if there is a problem + * writing to the output stream + */ + public void finish() throws IOException { + if (this.pagePending) { + closePage(); + } + + //Finish document + gen.writeDSCComment(DSCConstants.TRAILER); + gen.writeDSCComment(DSCConstants.PAGES, this.pagecount); + gen.writeDSCComment(DSCConstants.EOF); + gen.flush(); + } + + /** + * This constructor supports the create method + * @param g the PostScript document graphics to make a copy of + */ + public AbstractPSDocumentGraphics2D(AbstractPSDocumentGraphics2D g) { + super(g); + } + +} + diff --git a/src/main/java/org/apache/xmlgraphics/java2d/ps/EPSDocumentGraphics2D.java b/src/main/java/org/apache/xmlgraphics/java2d/ps/EPSDocumentGraphics2D.java new file mode 100644 index 0000000..7029591 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/java2d/ps/EPSDocumentGraphics2D.java @@ -0,0 +1,96 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: EPSDocumentGraphics2D.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.java2d.ps; + +import java.io.IOException; + +import org.apache.xmlgraphics.ps.DSCConstants; + +/** + * This class is a wrapper for the AbstractPSDocumentGraphics2D that + * is used to create EPS (Encapsulated PostScript) files instead of PS file. + * + * @version $Id: EPSDocumentGraphics2D.java 1732018 2016-02-24 04:51:06Z gadams $ + * @see org.apache.xmlgraphics.java2d.ps.PSGraphics2D + * @see org.apache.xmlgraphics.java2d.ps.AbstractPSDocumentGraphics2D + */ +public class EPSDocumentGraphics2D extends AbstractPSDocumentGraphics2D { + + /** + * Create a new EPSDocumentGraphics2D. + * This is used to create a new EPS document, the height, + * width and output stream can be setup later. + * For use by the transcoder which needs font information + * for the bridge before the document size is known. + * The resulting document is written to the stream after rendering. + * + * @param textAsShapes set this to true so that text will be rendered + * using curves and not the font. + */ + public EPSDocumentGraphics2D(boolean textAsShapes) { + super(textAsShapes); + } + + /** {@inheritDoc} */ + protected void writeFileHeader() throws IOException { + final Long pagewidth = (long) this.width; + final Long pageheight = (long) this.height; + + //PostScript Header + gen.writeln(DSCConstants.PS_ADOBE_30 + " " + DSCConstants.EPSF_30); + gen.writeDSCComment(DSCConstants.CREATOR, + new String[] {"Apache XML Graphics Commons" + + ": EPS Generator for Java2D"}); + gen.writeDSCComment(DSCConstants.CREATION_DATE, + new Object[] {new java.util.Date()}); + gen.writeDSCComment(DSCConstants.PAGES, DSCConstants.ATEND); + gen.writeDSCComment(DSCConstants.BBOX, new Object[] + {ZERO, ZERO, pagewidth, pageheight}); + gen.writeDSCComment(DSCConstants.LANGUAGE_LEVEL, gen.getPSLevel()); + gen.writeDSCComment(DSCConstants.END_COMMENTS); + + //Prolog + gen.writeDSCComment(DSCConstants.BEGIN_PROLOG); + writeProcSets(); + if (customTextHandler instanceof PSTextHandler) { + ((PSTextHandler)customTextHandler).writeSetup(); + } + gen.writeDSCComment(DSCConstants.END_PROLOG); + } + + /** {@inheritDoc} */ + protected void writePageHeader() throws IOException { + Integer pageNumber = this.pagecount; + gen.writeDSCComment(DSCConstants.PAGE, new Object[] + {pageNumber.toString(), pageNumber}); + gen.writeDSCComment(DSCConstants.PAGE_BBOX, new Object[] + {ZERO, ZERO, width, height}); + gen.writeDSCComment(DSCConstants.BEGIN_PAGE_SETUP); + if (customTextHandler instanceof PSTextHandler) { + ((PSTextHandler)customTextHandler).writePageSetup(); + } + } + + /** {@inheritDoc} */ + protected void writePageTrailer() throws IOException { + //nop, no DSC PageTrailer needed + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/java2d/ps/PSDocumentGraphics2D.java b/src/main/java/org/apache/xmlgraphics/java2d/ps/PSDocumentGraphics2D.java new file mode 100644 index 0000000..78854b8 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/java2d/ps/PSDocumentGraphics2D.java @@ -0,0 +1,145 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PSDocumentGraphics2D.java 1681108 2015-05-22 13:26:12Z ssteiner $ */ + +package org.apache.xmlgraphics.java2d.ps; + +import java.io.IOException; +import java.io.OutputStream; + +import org.apache.xmlgraphics.ps.DSCConstants; + +/** + * This class is a wrapper for the PSGraphics2D that + * is used to create a full document around the PostScript rendering from + * PSGraphics2D. + * + * @version $Id: PSDocumentGraphics2D.java 1681108 2015-05-22 13:26:12Z ssteiner $ + * @see org.apache.xmlgraphics.java2d.ps.PSGraphics2D + * + * Originally authored by Keiron Liddle. + */ +public class PSDocumentGraphics2D extends AbstractPSDocumentGraphics2D { + + /** + * Create a new AbstractPSDocumentGraphics2D. + * This is used to create a new PostScript document, the height, + * width and output stream can be setup later. + * For use by the transcoder which needs font information + * for the bridge before the document size is known. + * The resulting document is written to the stream after rendering. + * + * @param textAsShapes set this to true so that text will be rendered + * using curves and not the font. + */ + public PSDocumentGraphics2D(boolean textAsShapes) { + super(textAsShapes); + } + + /** + * Create a new AbstractPSDocumentGraphics2D. + * This is used to create a new PostScript document of the given height + * and width. + * The resulting document is written to the stream after rendering. + * + * @param textAsShapes set this to true so that text will be rendered + * using curves and not the font. + * @param stream the stream that the final document should be written to. + * @param width the width of the document + * @param height the height of the document + * @throws IOException an io exception if there is a problem + * writing to the output stream + */ + public PSDocumentGraphics2D(boolean textAsShapes, OutputStream stream, + int width, int height) throws IOException { + this(textAsShapes); + setupDocument(stream, width, height); + } + + /** {@inheritDoc} */ + public void nextPage() throws IOException { + closePage(); + } + + /** {@inheritDoc} */ + protected void writeFileHeader() throws IOException { + final Long pagewidth = (long) this.width; + final Long pageheight = (long) this.height; + + //PostScript Header + gen.writeln(DSCConstants.PS_ADOBE_30); + gen.writeDSCComment(DSCConstants.CREATOR, + new String[] {"Apache XML Graphics Commons" + + ": PostScript Generator for Java2D"}); + gen.writeDSCComment(DSCConstants.CREATION_DATE, + new Object[] {new java.util.Date()}); + gen.writeDSCComment(DSCConstants.PAGES, DSCConstants.ATEND); + gen.writeDSCComment(DSCConstants.BBOX, new Object[] + {ZERO, ZERO, pagewidth, pageheight}); + gen.writeDSCComment(DSCConstants.LANGUAGE_LEVEL, gen.getPSLevel()); + gen.writeDSCComment(DSCConstants.END_COMMENTS); + + //Defaults + gen.writeDSCComment(DSCConstants.BEGIN_DEFAULTS); + gen.writeDSCComment(DSCConstants.END_DEFAULTS); + + //Prolog + gen.writeDSCComment(DSCConstants.BEGIN_PROLOG); + gen.writeDSCComment(DSCConstants.END_PROLOG); + + //Setup + gen.writeDSCComment(DSCConstants.BEGIN_SETUP); + writeProcSets(); + if (customTextHandler instanceof PSTextHandler) { + ((PSTextHandler)customTextHandler).writeSetup(); + } + gen.writeDSCComment(DSCConstants.END_SETUP); + } + + /** {@inheritDoc} */ + protected void writePageHeader() throws IOException { + Integer pageNumber = this.pagecount; + gen.writeDSCComment(DSCConstants.PAGE, new Object[] + {pageNumber.toString(), pageNumber}); + gen.writeDSCComment(DSCConstants.PAGE_BBOX, new Object[] + {ZERO, ZERO, width, height}); + gen.writeDSCComment(DSCConstants.BEGIN_PAGE_SETUP); + gen.writeln("<<"); + gen.writeln("/PageSize [" + width + " " + height + "]"); + gen.writeln("/ImagingBBox null"); + gen.writeln(">> setpagedevice"); + if (customTextHandler instanceof PSTextHandler) { + ((PSTextHandler)customTextHandler).writePageSetup(); + } + } + + /** {@inheritDoc} */ + protected void writePageTrailer() throws IOException { + gen.showPage(); + } + + /** + * This constructor supports the create method + * @param g the PostScript document graphics to make a copy of + */ + public PSDocumentGraphics2D(PSDocumentGraphics2D g) { + super(g); + } + +} + diff --git a/src/main/java/org/apache/xmlgraphics/java2d/ps/PSGraphics2D.java b/src/main/java/org/apache/xmlgraphics/java2d/ps/PSGraphics2D.java new file mode 100644 index 0000000..5fc4398 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/java2d/ps/PSGraphics2D.java @@ -0,0 +1,962 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PSGraphics2D.java 1845492 2018-11-01 15:54:06Z ssteiner $ */ + +package org.apache.xmlgraphics.java2d.ps; + +//Java +import java.awt.AlphaComposite; +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Composite; +import java.awt.Dimension; +import java.awt.GradientPaint; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.GraphicsConfiguration; +import java.awt.Image; +import java.awt.Paint; +import java.awt.Rectangle; +import java.awt.Shape; +import java.awt.Stroke; +import java.awt.TexturePaint; +import java.awt.geom.AffineTransform; +import java.awt.geom.Area; +import java.awt.geom.Line2D; +import java.awt.geom.PathIterator; +import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; +import java.awt.image.ImageObserver; +import java.awt.image.RenderedImage; +import java.awt.image.renderable.RenderableImage; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.apache.xmlgraphics.java2d.AbstractGraphics2D; +import org.apache.xmlgraphics.java2d.GraphicContext; +import org.apache.xmlgraphics.java2d.GraphicsConfigurationWithoutTransparency; +import org.apache.xmlgraphics.java2d.StrokingTextHandler; +import org.apache.xmlgraphics.java2d.TextHandler; +import org.apache.xmlgraphics.ps.PSGenerator; +import org.apache.xmlgraphics.ps.PSImageUtils; + +/** + * This is a concrete implementation of AbstractGraphics2D (and + * therefore of Graphics2D) which is able to generate PostScript + * code. + * + * @version $Id: PSGraphics2D.java 1845492 2018-11-01 15:54:06Z ssteiner $ + * @see org.apache.xmlgraphics.java2d.AbstractGraphics2D + * + * Originally authored by Keiron Liddle. + */ +public class PSGraphics2D extends AbstractGraphics2D { + + private static final AffineTransform IDENTITY_TRANSFORM = new AffineTransform(); + + private static final boolean DEBUG = false; + + protected List pathHashCache = new ArrayList(); + protected boolean startCache; + /** + * The G2D instance that represents the root instance + * (used in context with create()/dispose()). Null if this instance is the root instance. + */ + protected PSGraphics2D rootG2D; + + /** the PostScript generator being created */ + protected PSGenerator gen; + + /** Disable or enable clipping */ + protected boolean clippingDisabled; + + /** Fallback text handler */ + + protected TextHandler fallbackTextHandler = new StrokingTextHandler(); + + /** Custom text handler */ + protected TextHandler customTextHandler; + + /** + * the current colour for use in svg + */ + protected Color currentColour = new Color(0, 0, 0); + + /** + * Create a new Graphics2D that generates PostScript code. + * @param textAsShapes True if text should be rendered as graphics + * @see org.apache.xmlgraphics.java2d.AbstractGraphics2D#AbstractGraphics2D(boolean) + */ + public PSGraphics2D(boolean textAsShapes) { + super(textAsShapes); + } + + /** + * Create a new Graphics2D that generates PostScript code. + * @param textAsShapes True if text should be rendered as graphics + * @param gen PostScript generator to use for output + * @see org.apache.xmlgraphics.java2d.AbstractGraphics2D#AbstractGraphics2D(boolean) + */ + public PSGraphics2D(boolean textAsShapes, PSGenerator gen) { + this(textAsShapes); + setPSGenerator(gen); + } + + /** + * Constructor for creating copies + * @param g parent PostScript Graphics2D + */ + public PSGraphics2D(PSGraphics2D g) { + super(g); + + this.rootG2D = (g.rootG2D != null ? g.rootG2D : g); + setPSGenerator(g.gen); + this.clippingDisabled = g.clippingDisabled; + //this.fallbackTextHandler is not copied + //TODO The customTextHandler should probably not be passed over just like that + //fallbackTextHandler, for example, has to be recreated to point to the sub-Graphics2D + //to get the text positioning right. This might require changes in the TextHandler interface + this.customTextHandler = g.customTextHandler; + this.currentColour = g.currentColour; + } + + /** + * Sets the PostScript generator + * @param gen the PostScript generator + */ + public void setPSGenerator(PSGenerator gen) { + this.gen = gen; + } + + /** @return the PostScript generator used by this instance. */ + public PSGenerator getPSGenerator() { + return this.gen; + } + + /** + * Sets the GraphicContext + * @param c GraphicContext to use + */ + public void setGraphicContext(GraphicContext c) { + gc = c; + //setPrivateHints(); + } + + /** @return the fallback TextHandler implementation */ + public TextHandler getFallbackTextHandler() { + return this.fallbackTextHandler; + } + + /** @return the custom TextHandler implementation */ + public TextHandler getCustomTextHandler() { + return this.customTextHandler; + } + + /** + * Sets a custom TextHandler implementation that is responsible for painting text. The default + * TextHandler paints all text as shapes. A custom implementation can implement text painting + * using text painting operators. + * @param handler the custom TextHandler implementation + */ + public void setCustomTextHandler(TextHandler handler) { + this.customTextHandler = handler; + } + + /* TODO Add me back at the right place!!! + private void setPrivateHints() { + setRenderingHint(RenderingHintsKeyExt.KEY_AVOID_TILE_PAINTING, + RenderingHintsKeyExt.VALUE_AVOID_TILE_PAINTING_ON); + }*/ + + /** + * Disable clipping on each draw command. + * + * @param b set to true to disable all clipping. + */ + public void disableClipping(boolean b) { + this.clippingDisabled = b; + } + + /** + * Creates a new Graphics object that is + * a copy of this Graphics object. + * @return a new graphics context that is a copy of + * this graphics context. + */ + public Graphics create() { + preparePainting(); + return new PSGraphics2D(this); + } + + /** + * Central handler for IOExceptions for this class. + * @param ioe IOException to handle + */ + public void handleIOException(IOException ioe) { + //TODO Surely, there's a better way to do this. + ioe.printStackTrace(); + } + + /** + * This method is used by AbstractPSDocumentGraphics2D to prepare a new page if + * necessary. + */ + public void preparePainting() { + //nop, used by AbstractPSDocumentGraphics2D + if (rootG2D != null) { + rootG2D.preparePainting(); + } + } + + /** + * Draws as much of the specified image as is currently available. + * The image is drawn with its top-left corner at + * (xy) in this graphics context's coordinate + * space. Transparent pixels in the image do not affect whatever + * pixels are already there. + *

    + * This method returns immediately in all cases, even if the + * complete image has not yet been loaded, and it has not been dithered + * and converted for the current output device. + *

    + * If the image has not yet been completely loaded, then + * drawImage returns false. As more of + * the image becomes available, the process that draws the image notifies + * the specified image observer. + * @param img the specified image to be drawn. + * @param x the x coordinate. + * @param y the y coordinate. + * @param observer object to be notified as more of + * the image is converted. + * @return True if the image has been fully drawn/loaded + * @see java.awt.Image + * @see java.awt.image.ImageObserver + * @see java.awt.image.ImageObserver#imageUpdate(java.awt.Image, int, int, int, int, int) + */ + public boolean drawImage(Image img, int x, int y, ImageObserver observer) { + return drawImage(img, x, y, observer, null); + } + + public boolean drawImage(Image img, int x, int y, ImageObserver observer, Color mask) { + preparePainting(); + if (DEBUG) { + System.out.println("drawImage: " + x + ", " + y + " " + img.getClass().getName()); + } + + final int width = img.getWidth(observer); + final int height = img.getHeight(observer); + if (width == -1 || height == -1) { + return false; + } + + Dimension size = new Dimension(width, height); + BufferedImage buf = buildBufferedImage(size); + + java.awt.Graphics2D g = buf.createGraphics(); + g.setComposite(AlphaComposite.SrcOver); + g.setBackground(new Color(1, 1, 1, 0)); + g.fillRect(0, 0, width, height); + g.clip(new Rectangle(0, 0, buf.getWidth(), buf.getHeight())); + + if (!g.drawImage(img, 0, 0, observer)) { + return false; + } + g.dispose(); + + try { + AffineTransform at = getTransform(); + gen.saveGraphicsState(); + gen.concatMatrix(at); + Shape imclip = getClip(); + writeClip(imclip); + PSImageUtils.renderBitmapImage(buf, x, y, width, height, gen, mask); + gen.restoreGraphicsState(); + } catch (IOException ioe) { + handleIOException(ioe); + } + + return true; + } + + /** + * Creates a buffered image. + * @param size dimensions of the image to be created + * @return the buffered image + */ + public BufferedImage buildBufferedImage(Dimension size) { + return new BufferedImage(size.width, size.height, + BufferedImage.TYPE_INT_ARGB); + } + + /** + * Draws as much of the specified image as has already been scaled + * to fit inside the specified rectangle. + *

    + * The image is drawn inside the specified rectangle of this + * graphics context's coordinate space, and is scaled if + * necessary. Transparent pixels do not affect whatever pixels + * are already there. + *

    + * This method returns immediately in all cases, even if the + * entire image has not yet been scaled, dithered, and converted + * for the current output device. + * If the current output representation is not yet complete, then + * drawImage returns false. As more of + * the image becomes available, the process that draws the image notifies + * the image observer by calling its imageUpdate method. + *

    + * A scaled version of an image will not necessarily be + * available immediately just because an unscaled version of the + * image has been constructed for this output device. Each size of + * the image may be cached separately and generated from the original + * data in a separate image production sequence. + * @param img the specified image to be drawn. + * @param x the x coordinate. + * @param y the y coordinate. + * @param width the width of the rectangle. + * @param height the height of the rectangle. + * @param observer object to be notified as more of + * the image is converted. + * @return True if the image has been fully loaded/drawn + * @see java.awt.Image + * @see java.awt.image.ImageObserver + * @see java.awt.image.ImageObserver#imageUpdate(java.awt.Image, int, int, int, int, int) + */ + public boolean drawImage(Image img, int x, int y, int width, int height, + ImageObserver observer) { + preparePainting(); + System.err.println("NYI: drawImage"); + return true; + } + + /** + * Disposes of this graphics context and releases + * any system resources that it is using. + * A Graphics object cannot be used after + * disposehas been called. + *

    + * When a Java program runs, a large number of Graphics + * objects can be created within a short time frame. + * Although the finalization process of the garbage collector + * also disposes of the same system resources, it is preferable + * to manually free the associated resources by calling this + * method rather than to rely on a finalization process which + * may not run to completion for a long period of time. + *

    + * Graphics objects which are provided as arguments to the + * paint and update methods + * of components are automatically released by the system when + * those methods return. For efficiency, programmers should + * call dispose when finished using + * a Graphics object only if it was created + * directly from a component or another Graphics object. + * @see java.awt.Graphics#finalize + * @see java.awt.Component#paint + * @see java.awt.Component#update + * @see java.awt.Component#getGraphics + * @see java.awt.Graphics#create + */ + public void dispose() { + this.gen = null; + this.fallbackTextHandler = null; + this.customTextHandler = null; + this.currentColour = null; + } + + /** + * Processes a Shape generating the necessary painting operations. + * @param s Shape to process + * @return the winding rule of the path defining the shape + * @throws IOException In case of an I/O problem. + */ + public int processShape(Shape s, boolean cached) throws IOException { + if (s instanceof Rectangle2D) { + // Special optimization in case of Rectangle Shape + Rectangle2D r = (Rectangle2D) s; + gen.defineRect(r.getX(), r.getY(), r.getWidth(), r.getHeight()); + return PathIterator.WIND_NON_ZERO; + } else { + PathIterator iter = s.getPathIterator(IDENTITY_TRANSFORM); + if (cached) { + processPathIteratorCached(s); + } else { + processPathIterator(iter); + } + return iter.getWindingRule(); + } + } + + protected String processPathIteratorToString(PathIterator iter) throws IOException { + StringBuilder cmd = new StringBuilder(); + double[] vals = new double[6]; + while (!iter.isDone()) { + int type = iter.currentSegment(vals); + switch (type) { + case PathIterator.SEG_CUBICTO: + cmd.append(gen.formatDouble(vals[0])).append(" ").append(gen.formatDouble(vals[1])).append(" ") + .append(gen.formatDouble(vals[2])).append(" ").append(gen.formatDouble(vals[3])).append(" ") + .append(gen.formatDouble(vals[4])).append(" ").append(gen.formatDouble(vals[5])).append(" ") + .append(gen.mapCommand("curveto")).append("\n"); + break; + case PathIterator.SEG_LINETO: + cmd.append(gen.formatDouble(vals[0])).append(" ").append(gen.formatDouble(vals[1])).append(" ") + .append(gen.mapCommand("lineto")).append("\n"); + break; + case PathIterator.SEG_MOVETO: + cmd.append(gen.formatDouble(vals[0])).append(" ").append(gen.formatDouble(vals[1])).append(" ") + .append(gen.mapCommand("moveto")).append("\n"); + break; + case PathIterator.SEG_QUADTO: + cmd.append(gen.formatDouble(vals[0])).append(" ").append(gen.formatDouble(vals[1])).append(" ") + .append(gen.formatDouble(vals[2])).append(" ").append(gen.formatDouble(vals[3])).append(" QT") + .append("\n"); + break; + case PathIterator.SEG_CLOSE: + cmd.append(gen.mapCommand("closepath")).append("\n"); + break; + default: + break; + } + iter.next(); + } + return cmd.toString().trim(); + } + + protected void processPathIteratorCached(Shape s) throws IOException { + String cmd = processPathIteratorToString(s.getPathIterator(IDENTITY_TRANSFORM)); + int hash = cmd.hashCode(); + if (!startCache) { + if (pathHashCache.contains(hash)) { + startCache = true; + pathHashCache.clear(); + } else { + gen.writeln(cmd); + pathHashCache.add(hash); + } + } + if (startCache) { + if (!pathHashCache.contains(hash)) { + gen.writeln("/f" + hash + "{" + cmd + "}def"); + pathHashCache.add(hash); + } + gen.writeln("f" + hash); + } + } + + /** + * Processes a path iterator generating the nexessary painting operations. + * @param iter PathIterator to process + * @throws IOException In case of an I/O problem. + */ + public void processPathIterator(PathIterator iter) throws IOException { + gen.writeln(processPathIteratorToString(iter)); + } + + /** + * Strokes the outline of a Shape using the settings of the + * current Graphics2D context. The rendering attributes + * applied include the Clip, Transform, + * Paint, Composite and + * Stroke attributes. + * @param s the Shape to be rendered + * @see #setStroke + * @see #setPaint + * @see java.awt.Graphics#setColor + * @see #transform + * @see #setTransform + * @see #clip + * @see #setClip + * @see #setComposite + */ + public void draw(Shape s) { + preparePainting(); + try { + gen.saveGraphicsState(); + + AffineTransform trans = getTransform(); + boolean newTransform = !trans.isIdentity(); + + if (newTransform) { + gen.concatMatrix(trans); + } + Shape imclip = getClip(); + if (shouldBeClipped(imclip, s)) { + writeClip(imclip); + } + establishColor(getColor()); + + applyPaint(getPaint(), false); + applyStroke(getStroke()); + + gen.writeln(gen.mapCommand("newpath")); + processShape(s, false); + doDrawing(false, true, false); + gen.restoreGraphicsState(); + } catch (IOException ioe) { + handleIOException(ioe); + } + } + + /** + * Determines if a shape interacts with a clipping region. + * @param clip Shape defining the clipping region + * @param s Shape to be drawn + * @return true if code for a clipping region needs to be generated. + */ + public boolean shouldBeClipped(Shape clip, Shape s) { + if (clip == null || s == null) { + return false; + } + if (s instanceof Line2D) { + //Line shapes don't work with intersections so always clip + return true; + } + Area as = new Area(s); + Area imclip = new Area(clip); + imclip.intersect(as); + return !imclip.equals(as); + } + + /** + * Establishes a clipping region + * @param s Shape defining the clipping region + */ + public void writeClip(Shape s) { + if (s == null) { + return; + } + if (!this.clippingDisabled) { + preparePainting(); + try { + gen.writeln(gen.mapCommand("newpath")); + processShape(s, false); + // clip area + gen.writeln(gen.mapCommand("clip")); + } catch (IOException ioe) { + handleIOException(ioe); + } + } + } + + /** + * Applies a new Paint object. + * @param paint Paint object to use + * @param fill True if to be applied for filling + */ + protected void applyPaint(Paint paint, boolean fill) { + preparePainting(); + if (paint instanceof GradientPaint) { + System.err.println("NYI: Gradient paint"); + } else if (paint instanceof TexturePaint) { + if (fill) { + try { + // create pattern with texture and use it for filling of a graphics object + PSTilingPattern psTilingPattern = new PSTilingPattern("Pattern1", + (TexturePaint)paint, 0, 0, 3, null); + gen.write(psTilingPattern.toString(gen.isAcrobatDownsample())); + gen.writeln("/Pattern " + gen.mapCommand("setcolorspace")); + gen.writeln(psTilingPattern.getName() + " " + gen.mapCommand("setcolor")); + } catch (IOException ioe) { + handleIOException(ioe); + } + } + } + } + + /** + * Applies a new Stroke object. + * @param stroke Stroke object to use + */ + protected void applyStroke(Stroke stroke) { + preparePainting(); + try { + applyStroke(stroke, gen); + } catch (IOException ioe) { + handleIOException(ioe); + } + } + + /** + * Applies a new Stroke object. + * @param stroke the Stroke instance + * @param gen the PS generator + * @throws IOException if an I/O error occurs + */ + public static void applyStroke(Stroke stroke, final PSGenerator gen) + throws IOException { + if (stroke instanceof BasicStroke) { + BasicStroke basicStroke = (BasicStroke)stroke; + + float[] da = basicStroke.getDashArray(); + if (da != null) { + StringBuffer sb = new StringBuffer("["); + for (int count = 0; count < da.length; count++) { + sb.append(gen.formatDouble(da[count])); + if (count < da.length - 1) { + sb.append(" "); + } + } + sb.append("] "); + float offset = basicStroke.getDashPhase(); + sb.append(gen.formatDouble(offset)); + gen.useDash(sb.toString()); + } else { + gen.useDash(null); + } + int ec = basicStroke.getEndCap(); + switch (ec) { + case BasicStroke.CAP_BUTT: + gen.useLineCap(0); + break; + case BasicStroke.CAP_ROUND: + gen.useLineCap(1); + break; + case BasicStroke.CAP_SQUARE: + gen.useLineCap(2); + break; + default: System.err.println("Unsupported line cap: " + ec); + } + + int lj = basicStroke.getLineJoin(); + switch (lj) { + case BasicStroke.JOIN_MITER: + gen.useLineJoin(0); + float ml = basicStroke.getMiterLimit(); + gen.useMiterLimit(ml >= -1 ? ml : 1); + break; + case BasicStroke.JOIN_ROUND: + gen.useLineJoin(1); + break; + case BasicStroke.JOIN_BEVEL: + gen.useLineJoin(2); + break; + default: System.err.println("Unsupported line join: " + lj); + } + float lw = basicStroke.getLineWidth(); + gen.useLineWidth(lw); + } else { + System.err.println("Stroke not supported: " + stroke.toString()); + } + } + + /** + * Renders a {@link RenderedImage}, + * applying a transform from image + * space into user space before drawing. + * The transformation from user space into device space is done with + * the current Transform in the Graphics2D. + * The specified transformation is applied to the image before the + * transform attribute in the Graphics2D context is applied. + * The rendering attributes applied include the Clip, + * Transform, and Composite attributes. Note + * that no rendering is done if the specified transform is + * noninvertible. + * @param img the image to be rendered + * @param xform the transformation from image space into user space + * @see #transform + * @see #setTransform + * @see #setComposite + * @see #clip + * @see #setClip + */ + public void drawRenderedImage(RenderedImage img, AffineTransform xform) { + preparePainting(); + try { + AffineTransform at = getTransform(); + gen.saveGraphicsState(); + gen.concatMatrix(at); + gen.concatMatrix(xform); + Shape imclip = getClip(); + writeClip(imclip); + PSImageUtils.renderBitmapImage(img, + 0, 0, img.getWidth(), img.getHeight(), gen, null); + gen.restoreGraphicsState(); + } catch (IOException ioe) { + handleIOException(ioe); + } + } + + /** + * Renders a + * {@link RenderableImage}, + * applying a transform from image space into user space before drawing. + * The transformation from user space into device space is done with + * the current Transform in the Graphics2D. + * The specified transformation is applied to the image before the + * transform attribute in the Graphics2D context is applied. + * The rendering attributes applied include the Clip, + * Transform, and Composite attributes. Note + * that no rendering is done if the specified transform is + * noninvertible. + *

    + * Rendering hints set on the Graphics2D object might + * be used in rendering the RenderableImage. + * If explicit control is required over specific hints recognized by a + * specific RenderableImage, or if knowledge of which hints + * are used is required, then a RenderedImage should be + * obtained directly from the RenderableImage + * and rendered using + * {@link #drawRenderedImage(RenderedImage, AffineTransform) drawRenderedImage}. + * @param img the image to be rendered + * @param xform the transformation from image space into user space + * @see #transform + * @see #setTransform + * @see #setComposite + * @see #clip + * @see #setClip + * @see #drawRenderedImage + */ + public void drawRenderableImage(RenderableImage img, + AffineTransform xform) { + preparePainting(); + System.err.println("NYI: drawRenderableImage"); + } + + /** + * Establishes the given color in the PostScript interpreter. + * @param c the color to set + * @throws IOException In case of an I/O problem + */ + public void establishColor(Color c) throws IOException { + gen.useColor(c); + } + + /** + * Renders the text specified by the specified String, + * using the current Font and Paint attributes + * in the Graphics2D context. + * The baseline of the first character is at position + * (xy) in the User Space. + * The rendering attributes applied include the Clip, + * Transform, Paint, Font and + * Composite attributes. For characters in script systems + * such as Hebrew and Arabic, the glyphs can be rendered from right to + * left, in which case the coordinate supplied is the location of the + * leftmost character on the baseline. + * @param s the String to be rendered + * @param x the x-coordinate where the String + * should be rendered + * @param y the y-coordinate where the String + * should be rendered + * @see #setPaint + * @see java.awt.Graphics#setColor + * @see java.awt.Graphics#setFont + * @see #setTransform + * @see #setComposite + * @see #setClip + */ + public void drawString(String s, float x, float y) { + try { + if (customTextHandler != null && !textAsShapes) { + customTextHandler.drawString(this, s, x, y); + } else { + fallbackTextHandler.drawString(this, s, x, y); + } + } catch (IOException ioe) { + handleIOException(ioe); + } + } + + /** + * Fills the interior of a Shape using the settings of the + * Graphics2D context. The rendering attributes applied + * include the Clip, Transform, + * Paint, and Composite. + * @param s the Shape to be filled + * @see #setPaint + * @see java.awt.Graphics#setColor + * @see #transform + * @see #setTransform + * @see #setComposite + * @see #clip + * @see #setClip + */ + public void fill(Shape s) { + if (!hasAlpha()) { + preparePainting(); + try { + gen.saveGraphicsState(); + + AffineTransform trans = getTransform(); + boolean newTransform = !trans.isIdentity(); + + if (newTransform) { + gen.concatMatrix(trans); + } + Shape imclip = getClip(); + if (shouldBeClipped(imclip, s)) { + writeClip(imclip); + } + + establishColor(getColor()); + + applyPaint(getPaint(), true); + + gen.writeln(gen.mapCommand("newpath")); + int windingRule = processShape(s, true); + doDrawing(true, false, + windingRule == PathIterator.WIND_EVEN_ODD); + gen.restoreGraphicsState(); + } catch (IOException ioe) { + handleIOException(ioe); + } + } + } + + private boolean hasAlpha() { + Composite composite = getComposite(); + return composite instanceof AlphaComposite && ((AlphaComposite) composite).getAlpha() == 0f; + } + + /** + * Commits a painting operation. + * @param fill filling + * @param stroke stroking + * @param nonzero true if the non-zero winding rule should be used when filling + * @exception IOException In case of an I/O problem + */ + protected void doDrawing(boolean fill, boolean stroke, boolean nonzero) + throws IOException { + preparePainting(); + if (fill) { + if (stroke) { + if (!nonzero) { + gen.writeln(gen.mapCommand("gsave") + " " + + gen.mapCommand("fill") + " " + + gen.mapCommand("grestore") + " " + + gen.mapCommand("stroke")); + } else { + gen.writeln(gen.mapCommand("gsave") + " " + + gen.mapCommand("eofill") + " " + + gen.mapCommand("grestore") + " " + + gen.mapCommand("stroke")); + } + } else { + if (!nonzero) { + gen.writeln(gen.mapCommand("fill")); + } else { + gen.writeln(gen.mapCommand("eofill")); + } + } + } else { + // if(stroke) + gen.writeln(gen.mapCommand("stroke")); + } + } + + /** + * Returns the device configuration associated with this + * Graphics2D. + * @return the device configuration + */ + public GraphicsConfiguration getDeviceConfiguration() { + return new GraphicsConfigurationWithoutTransparency(); + } + + /** + * Used to create proper font metrics + */ + private Graphics2D fmg; + + { + BufferedImage bi = new BufferedImage(1, 1, + BufferedImage.TYPE_INT_ARGB); + + fmg = bi.createGraphics(); + } + + /** + * Gets the font metrics for the specified font. + * @return the font metrics for the specified font. + * @param f the specified font + * @see java.awt.Graphics#getFont + * @see java.awt.FontMetrics + * @see java.awt.Graphics#getFontMetrics() + */ + public java.awt.FontMetrics getFontMetrics(java.awt.Font f) { + return fmg.getFontMetrics(f); + } + + /** + * Sets the paint mode of this graphics context to alternate between + * this graphics context's current color and the new specified color. + * This specifies that logical pixel operations are performed in the + * XOR mode, which alternates pixels between the current color and + * a specified XOR color. + *

    + * When drawing operations are performed, pixels which are the + * current color are changed to the specified color, and vice versa. + *

    + * Pixels that are of colors other than those two colors are changed + * in an unpredictable but reversible manner; if the same figure is + * drawn twice, then all pixels are restored to their original values. + * @param c1 the XOR alternation color + */ + public void setXORMode(Color c1) { + System.err.println("NYI: setXORMode"); + } + + + /** + * Copies an area of the component by a distance specified by + * dx and dy. From the point specified + * by x and y, this method + * copies downwards and to the right. To copy an area of the + * component to the left or upwards, specify a negative value for + * dx or dy. + * If a portion of the source rectangle lies outside the bounds + * of the component, or is obscured by another window or component, + * copyArea will be unable to copy the associated + * pixels. The area that is omitted can be refreshed by calling + * the component's paint method. + * @param x the x coordinate of the source rectangle. + * @param y the y coordinate of the source rectangle. + * @param width the width of the source rectangle. + * @param height the height of the source rectangle. + * @param dx the horizontal distance to copy the pixels. + * @param dy the vertical distance to copy the pixels. + */ + public void copyArea(int x, int y, int width, int height, int dx, + int dy) { + System.err.println("NYI: copyArea"); + } + + /* --- for debugging + public void transform(AffineTransform tx) { + System.out.println("transform(" + toArray(tx) + ")"); + super.transform(zx); + } + + public void scale(double sx, double sy) { + System.out.println("scale(" + sx + ", " + sy + ")"); + super.scale(sx, sy); + } + + public void translate(double tx, double ty) { + System.out.println("translate(double " + tx + ", " + ty + ")"); + super.translate(tx, ty); + } + + public void translate(int tx, int ty) { + System.out.println("translate(int " + tx + ", " + ty + ")"); + super.translate(tx, ty); + } + */ + +} diff --git a/src/main/java/org/apache/xmlgraphics/java2d/ps/PSTextHandler.java b/src/main/java/org/apache/xmlgraphics/java2d/ps/PSTextHandler.java new file mode 100644 index 0000000..91901e0 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/java2d/ps/PSTextHandler.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PSTextHandler.java 995366 2010-09-09 10:02:17Z jeremias $ */ + +package org.apache.xmlgraphics.java2d.ps; + +import java.io.IOException; + +import org.apache.xmlgraphics.java2d.TextHandler; + +/** + * Interface which the Graphics2D class delegates text painting to for Postscript. + */ +public interface PSTextHandler extends TextHandler { + /** + * Is called by when the "Setup" or "Prolog" of the PostScript document is generated. + * Subclasses can do font registration, for example. + * @throws IOException In case of an I/O error + */ + void writeSetup() throws IOException; + + /** + * Is called by when a "PageSetup" section of the PostScript document is generated. + * Subclasses can do some font initialization if necessary. + * @throws IOException In case of an I/O error + */ + void writePageSetup() throws IOException; +} diff --git a/src/main/java/org/apache/xmlgraphics/java2d/ps/PSTilingPattern.java b/src/main/java/org/apache/xmlgraphics/java2d/ps/PSTilingPattern.java new file mode 100644 index 0000000..6d7af24 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/java2d/ps/PSTilingPattern.java @@ -0,0 +1,578 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PSTilingPattern.java 1809627 2017-09-25 13:42:08Z ssteiner $ */ + +package org.apache.xmlgraphics.java2d.ps; + +import java.awt.AlphaComposite; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.TexturePaint; +import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; +import java.util.List; + +/** + * This class is implementation of PostScript tiling pattern. It allows to make a pattern + * with defined PaintProc or texture. + * + * Originally authored by Jiri Kunhart. + */ +public class PSTilingPattern { + + /** + * A code identifying the pattern type that this dictionary describes; + * must be 1 for a tiling pattern + */ + public static final int PATTERN_TYPE_TILING = 1; + + /** PostScript constant for a shading pattern (unsupported) */ + public static final int PATTERN_TYPE_SHADING = 2; + + /** the pattern type of this pattern */ + protected int patternType = PATTERN_TYPE_TILING; + //TODO To be moved to a super class once shading patterns are implemented. + + /** + * The name of the pattern (for example: "Pattern1" ) + */ + protected String patternName; + + /** + * The XUID is an extended unique ID -- an array of integers that provides for + * distributed, hierarchical management of the space of unique ID numbers + * (optional) + */ + protected List xUID; + + /** + * A PostScript procedure for painting the pattern cell + */ + protected StringBuffer paintProc; + + /** + * An array of four numbers in the pattern coordinate system, giving + * the coordinates of the left, bottom, right, and top edges, respectively, of the + * pattern cell's bounding box + */ + protected Rectangle2D bBox; + + /** + * The desired horizontal spacing between pattern cells, measured in + * the pattern coordinate system + */ + protected double xStep; + + /** + * The desired vertical spacing between pattern cells, measured in + * the pattern coordinate system + */ + protected double yStep; + + /** + * A code that determines how the color of the pattern cell is to be + * specified: 1 for colored pattern, 2 for uncolored pattern + */ + protected int paintType = 2; + + /** + * A code that controls adjustments to the spacing of tiles relative to + * the device pixel grid: + * 1 for constant spacing, + * 2 for no distortion + * 3 for constant spacing and faster tiling. + */ + protected int tilingType = 1; + + /** + * A texture is used for filling shapes + */ + protected TexturePaint texture; + + /** + * Constructor for the creation of pattern with defined PaintProc + * + * @param patternName the name of the pattern (for example: "Pattern1" ), if + * the name is null, the pattern should be stored in PSPatternStorage, where the pattern + * gets a name (the pattern without name cannot be use in PS file) + * @param paintProc a postscript procedure for painting the pattern cell + * @param bBox a pattern cell's bounding box + * @param xStep the desired horizontal spacing between pattern cells + * @param yStep the desired vertical spacing between pattern cells + * @param paintType 1 for colored pattern, 2 for uncolored pattern + * @param tilingType adjustments to the spacing of tiles relative to + * the device pixel grid (1,2 or 3) + * @param xUID an extended unique ID (optional) + */ + public PSTilingPattern(String patternName, StringBuffer paintProc, Rectangle bBox, + double xStep, double yStep, + int paintType, int tilingType, List xUID) { + + // check the parameters + this.patternName = patternName; + this.paintProc = paintProc; + setBoundingBox(bBox); + setXStep(xStep); + setYStep(yStep); + setPaintType(paintType); + setTilingType(tilingType); + this.xUID = xUID; + } + + /** + * Constructor for the creation of pattern with defined texture + * + * @param patternName the name of the pattern (for example: "Pattern1" ), if + * the name is null, the pattern should be stored in PSPatternStorage, where the pattern + * gets a name (a pattern without name cannot be use in PS file) + * @param texture a texture is used for filling a shape + * @param xStep the desired horizontal spacing between pattern cells + * @param yStep yStep the desired vertical spacing between pattern cells + * @param tilingType adjustments to the spacing of tiles relative to + * the device pixel grid (1,2 or 3) + * @param xUID xUID an extended unique ID (optional) + */ + public PSTilingPattern(String patternName, TexturePaint texture, double xStep, double yStep, + int tilingType, List xUID) { + + this(patternName, null, new Rectangle(), 1, 1, 1, tilingType, xUID); + + this.texture = texture; + + Rectangle2D anchor = texture.getAnchorRect(); + bBox = new Rectangle2D.Double( + anchor.getX(), anchor.getY(), + anchor.getX() + anchor.getWidth(), anchor.getY() + anchor.getHeight()); + + // xStep and yStep may be either positive or negative, but not zero => if it is zero, + // we set xStep and yStep in this way that the pattern will be without spaces + this.xStep = (xStep == 0) ? anchor.getWidth() : xStep; + this.yStep = (yStep == 0) ? anchor.getHeight() : yStep; + } + + /** + * Gets the name of the pattern + * + * @return String representing the name of the pattern. + */ + public String getName() { + return (this.patternName); + } + + /** + * Sets the name of the pattern. + * @param name the name of the pattern. Can be anything without spaces (for example "Pattern1"). + */ + public void setName(String name) { + if (name == null) { + throw new NullPointerException("Parameter patternName must not be null"); + } + if (name.length() == 0) { + throw new IllegalArgumentException("Parameter patternName must not be empty"); + } + if (name.indexOf(" ") >= 0) { + throw new IllegalArgumentException( + "Pattern name must not contain any spaces"); + } + this.patternName = name; + } + + /** + * Returns the bounding box. + * + * @return a pattern cell's bounding box + */ + public Rectangle2D getBoundingBox() { + return (this.bBox); + } + + /** + * Sets the bounding box. + * + * @param bBox a pattern cell's bounding box + */ + public void setBoundingBox(Rectangle2D bBox) { + if (bBox == null) { + throw new NullPointerException("Parameter bBox must not be null"); + } + this.bBox = bBox; + } + + /** + * Gets the postscript procedure PaintProc + * + * @return the postscript procedure PaintProc + */ + public StringBuffer getPaintProc() { + return (this.paintProc); + } + + /** + * Sets the postscript procedure PaintProc + * + * @param paintProc the postscript procedure PaintProc + */ + public void setPaintProc(StringBuffer paintProc) { + this.paintProc = paintProc; + } + + /** + * Gets the horizontal spacing between pattern cells + * + * @return the horizontal spacing between pattern cells + */ + public double getXStep() { + return (this.xStep); + } + + /** + * Sets the horizontal spacing between pattern cells + * + * @param xStep the horizontal spacing between pattern cells + */ + public void setXStep(double xStep) { + if (xStep == 0) { + throw new IllegalArgumentException("Parameter xStep must not be 0"); + } + this.xStep = xStep; + } + + /** + * Gets the vertical spacing between pattern cells + * + * @return the vertical spacing between pattern cells + */ + public double getYStep() { + return (this.yStep); + } + + /** + * Sets the vertical spacing between pattern cells + * + * @param yStep the vertical spacing between pattern cells + */ + public void setYStep(double yStep) { + if (yStep == 0) { + throw new IllegalArgumentException("Parameter yStep must not be 0"); + } + this.yStep = yStep; + } + + /** + * Gets the code that determines how the color of the pattern cell is to be + * specified: 1 for colored pattern, 2 for uncolored pattern + * + * @return the paint type + */ + public int getPaintType() { + return (this.paintType); + } + + /** + * Sets the code that determines how the color of the pattern cell is to be + * specified: 1 for colored pattern, 2 for uncolored pattern + * + * @param paintType the paint type + */ + public void setPaintType(int paintType) { + if ((paintType != 1) && (paintType != 2)) { + throw new IllegalArgumentException("Parameter paintType must not be " + + paintType + " (only 1 or 2)"); + } + this.paintType = paintType; + } + + /** + * Gets a code that controls adjustments to the spacing of tiles relative to + * the device pixel grid: 1 for constant spacing, 2 for no distortion + * 3 for constant spacing and faster tiling + * + * @return the tiling type + */ + public int getTilingType() { + return (this.tilingType); + } + + /** + * Sets a code that controls adjustments to the spacing of tiles relative to + * the device pixel grid: 1 for constant spacing, 2 for no distortion + * 3 for constant spacing and faster tiling + * + * @param tilingType the tiling type + */ + public void setTilingType(int tilingType) { + if (!((tilingType <= 3) && (tilingType >= 1))) { + throw new IllegalArgumentException("Parameter tilingType must not be " + + tilingType + " (only 1, 2 or 3)"); + } + this.tilingType = tilingType; + } + + /** + * Gets a texture which is used for filling shapes + * + * @return the texture + */ + public TexturePaint getTexturePaint() { + return (this.texture); + } + + /** + * Sets a texture which is used for filling shapes + * + * @param texturePaint the texture + */ + public void setTexturePaint(TexturePaint texturePaint) { + this.texture = texturePaint; + } + + /** + * Gets an extended unique ID that uniquely identifies the pattern + * + * @return xUID the unique ID + */ + public List getXUID() { + return (this.xUID); + } + + /** + * Sets an extended unique ID that uniquely identifies the pattern + * + * @param xUID the unique ID + */ + public void setXUID(List xUID) { + this.xUID = xUID; + } + + /** + * Generates postscript code for a pattern + * + * @return The string which contains postscript code of pattern definition + */ + public String toString(boolean acrobatDownsample) { + StringBuffer sb = new StringBuffer("<<\n"); + sb.append("/PatternType " + this.patternType + "\n"); + sb.append("/PaintType " + paintType + "\n"); + sb.append("/TilingType " + tilingType + "\n"); + sb.append("/XStep " + xStep + "\n"); + sb.append("/YStep " + yStep + "\n"); + sb.append("/BBox " + "[" + bBox.getX() + " " + bBox.getY() + " " + + bBox.getWidth() + " " + bBox.getHeight() + "]" + "\n"); + sb.append("/PaintProc\n" + "{\n"); + + // the PaintProc procedure is expected to consume its dictionary operand ! + if ((paintProc == null) || (paintProc.indexOf("pop") != 0)) { + sb.append("pop\n"); + } + + if (texture != null) { + int width = texture.getImage().getWidth(); + int height = texture.getImage().getHeight(); + + Rectangle2D anchor = texture.getAnchorRect(); + if (anchor.getX() != 0 || anchor.getY() != 0) { + sb.append(anchor.getX() + " " + anchor.getY() + " translate\n"); + } + double scaleX = anchor.getWidth() / width; + double scaleY = anchor.getHeight() / height; + if (scaleX != 1 || scaleY != 1) { + sb.append(scaleX + " " + scaleY + " scale\n"); + } + + // define color image: width height bits/comp matrix + // datasrc0 datasrcncomp-1 multi ncomp colorimage + // width height bits/comp matrix + int bits = 8; + if (acrobatDownsample) { + bits = 4; + } + sb.append(width).append(" ").append(height).append(" ").append(bits).append(" ").append("matrix\n"); + int [] argb = new int[width * height]; // datasrc0 datasrcncomp-1 + getAsRGB().getRGB(0, 0, width, height, argb, 0, width); + + writeImage(sb, argb, width, bits); + + sb.append(" false 3 colorimage"); // multi ncomp colorimage + } else { + sb.append(paintProc); + } + sb.append("\n} bind \n"); // the end of PaintProc + sb.append(">>\n"); + + // create pattern instance from prototype + sb.append("matrix\n"); + sb.append("makepattern\n"); + + // save pattern to current dictionary + sb.append("/" + patternName + " exch def\n"); + + return sb.toString(); + } + + private void writeImage(StringBuffer sb, int[] argb, int width, int bits) { + int count = 0; + sb.append("{<"); + for (int i = 0; i < argb.length; i++) { + if ((i % width == 0) || (count > 249)) { + sb.append('\n'); + count = 0; // line should not be longer than 255 characters + } + if (bits == 4) { + Color c = new Color(argb[i]); + int v = c.getRed() / 16; + String s = Integer.toHexString(v); + sb.append(s); + + v = c.getGreen() / 16; + s = Integer.toHexString(v); + sb.append(s); + + v = c.getBlue() / 16; + s = Integer.toHexString(v); + sb.append(s); + + count += 3; + } else { + // delete alpha canal and write to output + StringBuffer sRGB = new StringBuffer(Integer.toHexString(argb[i] & 0x00ffffff)); + if (sRGB.length() != 6) { + sRGB.insert(0, "000000"); // zero padding + sRGB = new StringBuffer(sRGB.substring(sRGB.length() - 6)); + } + sb.append(sRGB); + count += 6; + } + } + sb.append("\n>}"); + } + + private BufferedImage getAsRGB() { + BufferedImage img = texture.getImage(); + if (img.getType() != BufferedImage.TYPE_INT_RGB) { + BufferedImage buf = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_INT_RGB); + Graphics2D g = buf.createGraphics(); + g.setComposite(AlphaComposite.SrcOver); + g.setBackground(Color.white); + g.fillRect(0, 0, img.getWidth(), img.getHeight()); + g.drawImage(img, 0, 0, null); + g.dispose(); + return buf; + } + return img; + } + + /** {@inheritDoc} */ + public int hashCode() { + return + 0 + ^ patternType + ^ ((xUID != null) ? xUID.hashCode() : 0) + ^ ((paintProc != null) ? paintProc.hashCode() : 0) + ^ ((bBox != null) ? bBox.hashCode() : 0) + ^ Double.valueOf(xStep).hashCode() + ^ Double.valueOf(yStep).hashCode() + ^ paintType + ^ tilingType + ^ ((texture != null) ? texture.hashCode() : 0); + } + + /** + * Compares two patterns data (except their names). + * {@inheritDoc} + */ + public boolean equals(Object pattern) { + if (pattern == null) { + return false; + } + if (!(pattern instanceof PSTilingPattern)) { + return false; + } + if (this == pattern) { + return true; + } + + PSTilingPattern patternObj = (PSTilingPattern) pattern; + if (this.patternType != patternObj.patternType) { + return false; + } + + TexturePaint patternTexture = patternObj.getTexturePaint(); + + if (((patternTexture == null) && (texture != null)) + || ((patternTexture != null) && (texture == null))) { + return false; + } + + if ((patternTexture != null) && (texture != null)) { + // compare textures data + int width = texture.getImage().getWidth(); + int height = texture.getImage().getHeight(); + + int widthPattern = patternTexture.getImage().getWidth(); + int heightPattern = patternTexture.getImage().getHeight(); + + if (width != widthPattern) { + return false; + } + if (height != heightPattern) { + return false; + } + int [] rgbData = new int[width * height]; + int [] rgbDataPattern = new int[widthPattern * heightPattern]; + + texture.getImage().getRGB(0, 0, width, height, rgbData, 0, width); + patternTexture.getImage().getRGB(0, 0, widthPattern, heightPattern, + rgbDataPattern, 0, widthPattern); + + for (int i = 0; i < rgbData.length; i++) { + if (rgbData[i] != rgbDataPattern[i]) { + return false; + } + } + } else { + // compare PaintProc + if (!paintProc.toString().equals(patternObj.getPaintProc().toString())) { + return false; + } + } + + // compare other parameters + if (xStep != patternObj.getXStep()) { + return false; + } + if (yStep != patternObj.getYStep()) { + return false; + } + if (paintType != patternObj.getPaintType()) { + return false; + } + if (tilingType != patternObj.getTilingType()) { + return false; + } + if (!bBox.equals(patternObj.getBoundingBox())) { + return false; + } + if ((xUID != null) && (patternObj.getXUID() != null)) { + if (!xUID.equals(patternObj.getXUID())) { + return false; + } + } + return true; + } +} diff --git a/src/main/java/org/apache/xmlgraphics/java2d/ps/package.html b/src/main/java/org/apache/xmlgraphics/java2d/ps/package.html new file mode 100644 index 0000000..3fe20e0 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/java2d/ps/package.html @@ -0,0 +1,23 @@ + + + +org.apache.xmlgraphics.commons.java2d.ps Package + +

    Graphics2D implementations for generating PostScript and Encapsulated PostScript (EPS) files.

    + + \ No newline at end of file diff --git a/src/main/java/org/apache/xmlgraphics/overview.html b/src/main/java/org/apache/xmlgraphics/overview.html new file mode 100644 index 0000000..c52e7f4 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/overview.html @@ -0,0 +1,12 @@ + +Apache XML Graphics Commons Overview + +

    Apache XML Graphics Commons is a library in which the two XML Graphics subprojects +(Apache Batik and Apache FOP) extracted components which are used by both codebases. +The goal is to create a clean dependency hierarchy and to make components more visible +which can be usable separately (i.e. without Batik and FOP).

    + +

    To more information, including general documentation, go to the Apache XML Graphics Project Website.

    + + \ No newline at end of file diff --git a/src/main/java/org/apache/xmlgraphics/ps/DSCConstants.java b/src/main/java/org/apache/xmlgraphics/ps/DSCConstants.java new file mode 100644 index 0000000..5109524 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/DSCConstants.java @@ -0,0 +1,245 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DSCConstants.java 1345683 2012-06-03 14:50:33Z gadams $ */ + +package org.apache.xmlgraphics.ps; + +/** + * This class defines constants with Strings for the DSC specification. + * + * @version $Id: DSCConstants.java 1345683 2012-06-03 14:50:33Z gadams $ + */ +public final class DSCConstants { + + private DSCConstants() { + } + + // ----==== General Header Comments ====---- + + /** Lead-in for a DSC-conformant PostScript file */ + public static final String PS_ADOBE_30 = "%!PS-Adobe-3.0"; + + /** Lead-in for an EPS file */ + public static final String EPSF_30 = "EPSF-3.0"; + + /** Bounding box for the document */ + public static final String BBOX = "BoundingBox"; + /** High-resolution bounding box for the document */ + public static final String HIRES_BBOX = "HiResBoundingBox"; + /** Copyright information associated with the document or resource */ + public static final String COPYRIGHT = "Copyright"; + /** Creator of the document */ + public static final String CREATOR = "Creator"; + /** Date and time when the document was created */ + public static final String CREATION_DATE = "CreationDate"; + /** Type of data */ + public static final String DOCUMENT_DATA = "BoundingBox"; + /** Use for indicating an emulator being invoked in the document */ + public static final String EMULATION = "Emulation"; + /** Explicit end of comments */ + public static final String END_COMMENTS = "EndComments"; + /** Required PostScript Level 1 extension for this document */ + public static final String EXTENSIONS = "Extensions"; + /** Indicates who is this document printed for */ + public static final String FOR = "For"; + /** Indicates the PostScript language level used in the document */ + public static final String LANGUAGE_LEVEL = "LanguageLevel"; + /** Indicates the orientation of the document */ + public static final String ORIENTATION = "Orientation"; + /** Number of pages in the document */ + public static final String PAGES = "Pages"; + /** Indicates the order of the pages */ + public static final String PAGE_ORDER = "PageOrder"; + /** Indicates how the document should be routed back to its owner */ + public static final String ROUTING = "Routing"; + /** Title of the document */ + public static final String TITLE = "Title"; + /** Version of the document */ + public static final String VERSION = "Version"; + + // ----==== General Body Comments ====---- + + /** Indicates a continued line */ + public static final String NEXT_LINE = "+ "; + + //Skipping BeginBinary/EndBinary. They are deprecated. + + /** Indicates the start of a data section*/ + public static final String BEGIN_DATA = "BeginData"; + /** Indicates the end of a data section*/ + public static final String END_DATA = "EndData"; + + /** Indicates the start of the defaults section */ + public static final String BEGIN_DEFAULTS = "BeginDefaults"; + /** Indicates the end of the defaults section */ + public static final String END_DEFAULTS = "EndDefaults"; + + /** Indicates the start of a non-PostScript section */ + public static final String BEGIN_EMULATION = "BeginEmulation"; + /** Indicates the end of a non-PostScript section */ + public static final String END_EMULATION = "EndEmulation"; + + /** Indicates the start of a preview section (EPS only)*/ + public static final String BEGIN_PREVIEW = "BeginPreview"; + /** Indicates the end of a preview section (EPS only)*/ + public static final String END_PREVIEW = "EndPreview"; + + /** Indicates the start of the prolog */ + public static final String BEGIN_PROLOG = "BeginProlog"; + /** Indicates the end of the prolog */ + public static final String END_PROLOG = "EndProlog"; + + /** Indicates the start of the document setup */ + public static final String BEGIN_SETUP = "BeginSetup"; + /** Indicates the end of the document setup */ + public static final String END_SETUP = "EndSetup"; + + + // ----==== General Page Comments ====---- + + /** Indicates the start of a graphic object */ + public static final String BEGIN_OBJECT = "BeginObject"; + /** Indicates the end of a graphic object */ + public static final String END_OBJECT = "EndObject"; + + /** Indicates the start of the page setup section */ + public static final String BEGIN_PAGE_SETUP = "BeginPageSetup"; + /** Indicates the end of the page setup section */ + public static final String END_PAGE_SETUP = "EndPageSetup"; + + /** Indicates a page number */ + public static final String PAGE = "Page"; + /** Bounding box for a page */ + public static final String PAGE_BBOX = "PageBoundingBox"; + /** High-resolution bounding box for a page */ + public static final String PAGE_HIRES_BBOX = "PageHiResBoundingBox"; + /** Bounding box for a page */ + public static final String PAGE_ORIENTATION = "PageOrientation"; + + + // ----==== General Trailer Comments ====---- + + /** Indicates the start of the page trailer */ + public static final String PAGE_TRAILER = "PageTrailer"; + /** Indicates the start of the document trailer */ + public static final String TRAILER = "Trailer"; + /** + * Indicates the end of a page (NON-STANDARD!) + * @deprecated Shouldn't really use that. Bad idea. "Page" and "Trailer" end a page. + */ + public static final String END_PAGE = "EndPage"; + /** Indicates the end of the document */ + public static final String EOF = "EOF"; + + + // ----==== Requirements Conventions ====---- + + /** + * This comment indicates all types of paper media (paper sizes, weight, color) + * this document requires. + */ + public static final String DOCUMENT_MEDIA = "DocumentMedia"; + /** This comment provides a list of resources the document needs */ + public static final String DOCUMENT_NEEDED_RESOURCES = "DocumentNeededResources"; + /** This comment provides a list of resources the document includes */ + public static final String DOCUMENT_SUPPLIED_RESOURCES = "DocumentSuppliedResources"; + //Skipping %%DocumentPrinterRequired + //Skipping %%DocumentNeededFiles -> deprecated + //Skipping %%DocumentSuppliedFiles -> deprecated + //Skipping %%DocumentFonts -> deprecated + //Skipping %%DocumentNeededFonts -> deprecated + //Skipping %%DocumentSuppliedFonts -> deprecated + //Skipping %%DocumentNeededProcSets -> deprecated + //Skipping %%DocumentSuppliedProcSets -> deprecated + //Skipping %%OperatorIntervention + //Skipping %%OperatorMessage + //Skipping %%ProofMode + /** + * This comment describes document requirements, such as duplex printing, + * hole punching, collating, or other physical document processing needs. + */ + public static final String REQUIREMENTS = "Requirements"; + //Skipping %%VMlocation + //Skipping %%VMusage + + // ----==== Requirement Body Comments ====---- + + /** Indicates the start of an embedded document */ + public static final String BEGIN_DOCUMENT = "BeginDocument"; + /** Indicates the end of an embedded document */ + public static final String END_DOCUMENT = "EndDocument"; + /** Indicates a referenced embedded document */ + public static final String INCLUDE_DOCUMENT = "IncludeDocument"; + + /** Indicates the start of a PPD feature */ + public static final String BEGIN_FEATURE = "BeginFeature"; + /** Indicates the end of a PPD feature */ + public static final String END_FEATURE = "EndFeature"; + /** Indicates a referenced a PPD feature */ + public static final String INCLUDE_FEATURE = "IncludeFeature"; + + //Skipping BeginFile/EndFile/IncludeFile. They are deprecated. + //Skipping BeginFont/EndFont/IncludeFont. They are deprecated. + //Skipping BeginProcSet/EndProcSet/IncludeProcSet. They are deprecated. + + /** Indicates the start of a resource (font, file, procset) */ + public static final String BEGIN_RESOURCE = "BeginResource"; + /** Indicates the end of a resource (font, file, procset) */ + public static final String END_RESOURCE = "EndResource"; + /** Indicates a referenced a resource (font, file, procset) */ + public static final String INCLUDE_RESOURCE = "IncludeResource"; + + // ----==== Requirement Page Comments ====---- + + //Skipping %%PageFonts -> deprecated + //Skipping %%PageFiles -> deprecated + /** Indicates that the paper attributes denoted by medianame are invoked on this page. */ + public static final String PAGE_MEDIA = "PageMedia"; + /** + * This is the page-level invocation of a combination of the options listed in + * the %%Requirements: comment. + */ + public static final String PAGE_REQUIREMENTS = "PageRequirements"; + /** + * This comment indicates the names and values of all resources that are needed + * or supplied on the present page. + */ + public static final String PAGE_RESOURCES = "PageResources"; + + // ----==== (atend) indicator ====---- + + /** + * Indicator for the PostScript interpreter that the value is provided + * later in the document (mostly in the %%Trailer section). + */ + public static final Object ATEND = new AtendIndicator(); + + /** Used for the ATEND constant. See there. */ + private static final class AtendIndicator extends Object { + + private AtendIndicator() { + super(); + } + + public String toString() { + return "(atend)"; + } + } + + +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/FormGenerator.java b/src/main/java/org/apache/xmlgraphics/ps/FormGenerator.java new file mode 100644 index 0000000..842a3d1 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/FormGenerator.java @@ -0,0 +1,138 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: FormGenerator.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.ps; + +import java.awt.geom.AffineTransform; +import java.awt.geom.Dimension2D; +import java.awt.geom.Rectangle2D; +import java.io.IOException; + +/** + * Abstract helper class for generating PostScript forms. + */ +public abstract class FormGenerator { + + private String formName; + private String title; + private Dimension2D dimensions; + + /** + * Main constructor. + * @param formName the form's name + * @param title the form's title or null + * @param dimensions the form's dimensions + */ + public FormGenerator(String formName, String title, Dimension2D dimensions) { + this.formName = formName; + this.title = title; + this.dimensions = dimensions; + } + + /** + * Returns the form's name. + * @return the form's name + */ + public String getFormName() { + return this.formName; + } + + /** + * Returns the form's title. + * @return the form's title or null if there's no title + */ + public String getTitle() { + return this.title; + } + + /** + * returns the form's dimensions. + * @return the form's dimensions + */ + public Dimension2D getDimensions() { + return this.dimensions; + } + + /** + * Generates the PostScript code for the PaintProc of the form. + * @param gen the PostScript generator + * @throws IOException if an I/O error occurs + */ + protected abstract void generatePaintProc(PSGenerator gen) throws IOException; + + /** + * Generates some PostScript code right after the form definition (used primarily for + * bitmap data). + * @param gen the PostScript generator + * @throws IOException if an I/O error occurs + */ + protected void generateAdditionalDataStream(PSGenerator gen) throws IOException { + //nop + } + + /** + * Returns the matrix for use in the form. + * @return the matrix + */ + protected AffineTransform getMatrix() { + return new AffineTransform(); + } + + /** + * Returns the form's bounding box. + * @return the form's bounding box + */ + protected Rectangle2D getBBox() { + return new Rectangle2D.Double(0, 0, dimensions.getWidth(), dimensions.getHeight()); + } + + /** + * Generates the PostScript form. + * @param gen the PostScript generator + * @return a PSResource instance representing the form + * @throws IOException if an I/O error occurs + */ + public PSResource generate(PSGenerator gen) throws IOException { + if (gen.getPSLevel() < 2) { + throw new UnsupportedOperationException( + "Forms require at least Level 2 PostScript"); + } + gen.writeDSCComment(DSCConstants.BEGIN_RESOURCE, + new Object[] {PSResource.TYPE_FORM, getFormName()}); + if (title != null) { + gen.writeDSCComment(DSCConstants.TITLE, title); + } + gen.writeln("/" + formName); + gen.writeln("<< /FormType 1"); + gen.writeln(" /BBox " + gen.formatRectangleToArray(getBBox())); + gen.writeln(" /Matrix " + gen.formatMatrix(getMatrix())); + gen.writeln(" /PaintProc {"); + gen.writeln(" pop"); + gen.writeln(" gsave"); + generatePaintProc(gen); + gen.writeln(" grestore"); + gen.writeln(" } bind"); + gen.writeln(">> def"); + generateAdditionalDataStream(gen); + gen.writeDSCComment(DSCConstants.END_RESOURCE); + PSResource res = new PSResource(PSResource.TYPE_FORM, formName); + gen.getResourceTracker().registerSuppliedResource(res); + return res; + } +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/ImageEncoder.java b/src/main/java/org/apache/xmlgraphics/ps/ImageEncoder.java new file mode 100644 index 0000000..f82f67f --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/ImageEncoder.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageEncoder.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.ps; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * The interface is implemented by classes that can generate the raw bitmap field for an image + * that might be further encoded/compressed by the image handler class. + */ +public interface ImageEncoder { + + /** + * Writes the whole raw bitmap field to the given OutputStream. The implementation must not + * close the OutputStream when it is finished! + * @param out the OutputStream to write to + * @throws IOException if an I/O error occurs + */ + void writeTo(OutputStream out) throws IOException; + + String getImplicitFilter(); + +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/ImageEncodingHelper.java b/src/main/java/org/apache/xmlgraphics/ps/ImageEncodingHelper.java new file mode 100644 index 0000000..48d32e4 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/ImageEncodingHelper.java @@ -0,0 +1,518 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageEncodingHelper.java 1896317 2021-12-23 14:30:49Z ssteiner $ */ + +package org.apache.xmlgraphics.ps; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.color.ColorSpace; +import java.awt.image.BufferedImage; +import java.awt.image.ColorModel; +import java.awt.image.ComponentColorModel; +import java.awt.image.DataBuffer; +import java.awt.image.DataBufferByte; +import java.awt.image.DirectColorModel; +import java.awt.image.IndexColorModel; +import java.awt.image.PixelInterleavedSampleModel; +import java.awt.image.Raster; +import java.awt.image.RenderedImage; +import java.awt.image.SampleModel; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Arrays; + +import org.apache.xmlgraphics.image.GraphicsUtil; + +/** + * Helper class for encoding bitmap images. + */ +public class ImageEncodingHelper { + + private static final ColorModel DEFAULT_RGB_COLOR_MODEL = new ComponentColorModel( + ColorSpace.getInstance(ColorSpace.CS_sRGB), + false, false, ColorModel.OPAQUE, DataBuffer.TYPE_BYTE); + + private final RenderedImage image; + private ColorModel encodedColorModel; + private boolean firstTileDump; + private boolean enableCMYK; + private boolean isBGR; + private boolean isKMYC; + private boolean outputbw; + private boolean bwinvert; + + /** + * Main constructor + * @param image the image + */ + public ImageEncodingHelper(RenderedImage image) { + this(image, true); + outputbw = true; + } + + /** + * Main constructor + * @param image the image + * @param enableCMYK true to enable CMYK, false to disable + */ + public ImageEncodingHelper(RenderedImage image, boolean enableCMYK) { + this.image = image; + this.enableCMYK = enableCMYK; + determineEncodedColorModel(); + } + + /** + * Returns the associated image. + * @return the image + */ + public RenderedImage getImage() { + return this.image; + } + + /** + * Returns the native {@link ColorModel} used by the image. + * @return the native color model + */ + public ColorModel getNativeColorModel() { + return getImage().getColorModel(); + } + + /** + * Returns the effective {@link ColorModel} used to encode the image. If this is different + * from the value returned by {@link #getNativeColorModel()} this means that the image + * is converted in order to encode it because no native encoding is currently possible. + * @return the effective color model + */ + public ColorModel getEncodedColorModel() { + return this.encodedColorModel; + } + + /** + * Indicates whether the image has an alpha channel. + * @return true if the image has an alpha channel + */ + public boolean hasAlpha() { + return image.getColorModel().hasAlpha(); + } + + /** + * Indicates whether the image is converted during encodation. + * @return true if the image cannot be encoded in its native format + */ + public boolean isConverted() { + return getNativeColorModel() != getEncodedColorModel(); + } + + private void writeRGBTo(OutputStream out) throws IOException { + boolean encoded = encodeRenderedImageWithDirectColorModelAsRGB(image, out); + if (encoded) { + return; + } + encodeRenderedImageAsRGB(image, out, outputbw, bwinvert); + } + + public static void encodeRenderedImageAsRGB(RenderedImage image, OutputStream out) + throws IOException { + encodeRenderedImageAsRGB(image, out, false, false); + } + + /** + * Writes a RenderedImage to an OutputStream by converting it to RGB. + * @param image the image + * @param out the OutputStream to write the pixels to + * @throws IOException if an I/O error occurs + */ + public static void encodeRenderedImageAsRGB(RenderedImage image, OutputStream out, + boolean outputbw, boolean bwinvert) throws IOException { + Raster raster = getRaster(image); + Object data; + int nbands = raster.getNumBands(); + int dataType = raster.getDataBuffer().getDataType(); + switch (dataType) { + case DataBuffer.TYPE_BYTE: + data = new byte[nbands]; + break; + case DataBuffer.TYPE_USHORT: + data = null; + break; + case DataBuffer.TYPE_INT: + data = new int[nbands]; + break; + case DataBuffer.TYPE_FLOAT: + data = new float[nbands]; + break; + case DataBuffer.TYPE_DOUBLE: + data = new double[nbands]; + break; + default: + throw new IllegalArgumentException("Unknown data buffer type: " + dataType); + } + + ColorModel colorModel = image.getColorModel(); + int w = image.getWidth(); + int h = image.getHeight(); + int numDataElements = 3; + if (colorModel.getPixelSize() == 1 && outputbw) { + numDataElements = 1; + } + + byte[] buf = new byte[w * numDataElements]; + + for (int y = 0; y < h; y++) { + int idx = -1; + for (int x = 0; x < w; x++) { + int rgb = colorModel.getRGB(raster.getDataElements(x, y, data)); + if (numDataElements > 1) { + buf[++idx] = (byte)(rgb >> 16); + buf[++idx] = (byte)(rgb >> 8); + } else if (bwinvert && rgb == -1) { + rgb = 1; + } + buf[++idx] = (byte)(rgb); + } + out.write(buf); + } + } + + /** + * Writes a RenderedImage to an OutputStream. This method optimizes the encoding + * of the {@link DirectColorModel} as it is returned by {@link ColorModel#getRGBdefault}. + * @param image the image + * @param out the OutputStream to write the pixels to + * @return true if this method encoded this image, false if the image is incompatible + * @throws IOException if an I/O error occurs + */ + public static boolean encodeRenderedImageWithDirectColorModelAsRGB( + RenderedImage image, OutputStream out) throws IOException { + ColorModel cm = image.getColorModel(); + if (cm.getColorSpace() != ColorSpace.getInstance(ColorSpace.CS_sRGB)) { + return false; //Need to go through color management + } + if (!(cm instanceof DirectColorModel)) { + return false; //Only DirectColorModel is supported here + } + DirectColorModel dcm = (DirectColorModel)cm; + final int[] templateMasks = new int[] + {0x00ff0000 /*R*/, 0x0000ff00 /*G*/, 0x000000ff /*B*/, 0xff000000 /*A*/}; + int[] masks = dcm.getMasks(); + if (!Arrays.equals(templateMasks, masks)) { + return false; //no flexibility here right now, might never be used anyway + } + + Raster raster = getRaster(image); + int dataType = raster.getDataBuffer().getDataType(); + if (dataType != DataBuffer.TYPE_INT) { + return false; //not supported + } + + int w = image.getWidth(); + int h = image.getHeight(); + + int[] data = new int[w]; + byte[] buf = new byte[w * 3]; + for (int y = 0; y < h; y++) { + int idx = -1; + raster.getDataElements(0, y, w, 1, data); + for (int x = 0; x < w; x++) { + int rgb = data[x]; + buf[++idx] = (byte)(rgb >> 16); + buf[++idx] = (byte)(rgb >> 8); + buf[++idx] = (byte)(rgb); + } + out.write(buf); + } + + return true; + } + + private static Raster getRaster(RenderedImage image) { + if (image instanceof BufferedImage) { + return ((BufferedImage)image).getRaster(); + } else { + //Note: this copies the image data (double memory consumption) + //TODO Investigate encoding in stripes: RenderedImage.copyData(WritableRaster) + return image.getData(); + } + } + + /** + * Converts a byte array containing 24 bit RGB image data to a grayscale + * image. + * + * @param raw + * the buffer containing the RGB image data + * @param width + * the width of the image in pixels + * @param height + * the height of the image in pixels + * @param bitsPerPixel + * the number of bits to use per pixel + * @param out the OutputStream to write the pixels to + * + * @throws IOException if an I/O error occurs + */ + public static void encodeRGBAsGrayScale( + byte[] raw, int width, int height, int bitsPerPixel, OutputStream out) + throws IOException { + int pixelsPerByte = 8 / bitsPerPixel; + int bytewidth = (width / pixelsPerByte); + if ((width % pixelsPerByte) != 0) { + bytewidth++; + } + + //TODO Rewrite to encode directly from a RenderedImage to avoid buffering the whole RGB + //image in memory + byte[] linedata = new byte[bytewidth]; + byte ib; + for (int y = 0; y < height; y++) { + ib = 0; + int i = 3 * y * width; + for (int x = 0; x < width; x++, i += 3) { + + // see http://www.jguru.com/faq/view.jsp?EID=221919 + double greyVal = 0.212671d * (raw[i] & 0xff) + 0.715160d + * (raw[i + 1] & 0xff) + 0.072169d + * (raw[i + 2] & 0xff); + switch (bitsPerPixel) { + case 1: + if (greyVal < 128) { + ib |= (byte) (1 << (7 - (x % 8))); + } + break; + case 4: + greyVal /= 16; + ib |= (byte) ((byte) greyVal << ((1 - (x % 2)) * 4)); + break; + case 8: + ib = (byte) greyVal; + break; + default: + throw new UnsupportedOperationException( + "Unsupported bits per pixel: " + bitsPerPixel); + } + + if ((x % pixelsPerByte) == (pixelsPerByte - 1) + || ((x + 1) == width)) { + linedata[(x / pixelsPerByte)] = ib; + ib = 0; + } + } + out.write(linedata); + } + } + + private boolean optimizedWriteTo(OutputStream out) + throws IOException { + if (this.firstTileDump) { + Raster raster = image.getTile(0, 0); + DataBuffer buffer = raster.getDataBuffer(); + if (buffer instanceof DataBufferByte) { + byte[] bytes = ((DataBufferByte) buffer).getData(); + // see determineEncodingColorModel() to see why we permute B and R here + if (isBGR) { + byte[] bytesPermutated = new byte[bytes.length]; + for (int i = 0; i < bytes.length; i += 3) { + bytesPermutated[i] = bytes[i + 2]; + bytesPermutated[i + 1] = bytes[i + 1]; + bytesPermutated[i + 2] = bytes[i]; + } + out.write(bytesPermutated); + } else if (isKMYC) { + byte[] bytesPermutated = new byte[bytes.length]; + for (int i = 0; i < bytes.length; i += 4) { + bytesPermutated[i] = bytes[i + 3]; + bytesPermutated[i + 1] = bytes[i + 2]; + bytesPermutated[i + 2] = bytes[i + 1]; + bytesPermutated[i + 3] = bytes[i]; + } + out.write(bytesPermutated); + } else { + out.write(bytes); + } + return true; + } + } + return false; + } + + /** + * Indicates whether the image consists of multiple tiles. + * @return true if there are multiple tiles + */ + protected boolean isMultiTile() { + int tilesX = image.getNumXTiles(); + int tilesY = image.getNumYTiles(); + return (tilesX != 1 || tilesY != 1); + } + + /** + * Determines the color model used for encoding the image. + */ + protected void determineEncodedColorModel() { + this.firstTileDump = false; + this.encodedColorModel = DEFAULT_RGB_COLOR_MODEL; + + ColorModel cm = image.getColorModel(); + ColorSpace cs = cm.getColorSpace(); + + int numComponents = cm.getNumComponents(); + + if (!isMultiTile()) { + if (numComponents == 1 && cs.getType() == ColorSpace.TYPE_GRAY) { + if (cm.getTransferType() == DataBuffer.TYPE_BYTE) { + this.firstTileDump = true; + this.encodedColorModel = cm; + } + } else if (cm instanceof IndexColorModel) { + if (cm.getTransferType() == DataBuffer.TYPE_BYTE) { + this.firstTileDump = true; + this.encodedColorModel = cm; + } + } else if (cm instanceof ComponentColorModel + && (numComponents == 3 || (enableCMYK && numComponents == 4)) + && !cm.hasAlpha()) { + Raster raster = image.getTile(0, 0); + DataBuffer buffer = raster.getDataBuffer(); + SampleModel sampleModel = raster.getSampleModel(); + if (sampleModel instanceof PixelInterleavedSampleModel) { + PixelInterleavedSampleModel piSampleModel; + piSampleModel = (PixelInterleavedSampleModel)sampleModel; + int[] offsets = piSampleModel.getBandOffsets(); + for (int i = 0; i < offsets.length; i++) { + if (offsets[i] != i && offsets[i] != offsets.length - 1 - i) { + //Don't encode directly as samples are not next to each other + //i.e. offsets are not 012 (RGB) or 0123 (CMYK) + // let also pass 210 BGR and 3210 (KYMC); 3210 will be skipped below + // if 210 (BGR) the B and R bytes will be permuted later in optimizeWriteTo() + return; + } + } + // check if we are in a BGR case; this is added here as a workaround for bug fix + // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6549882 that causes some PNG + // images to be loaded as BGR with the consequence that performance was being impacted + this.isBGR = false; + if (offsets.length == 3 && offsets[0] == 2 && offsets[1] == 1 && offsets[2] == 0) { + this.isBGR = true; + } + // make sure we did not get here due to a KMYC image + if (offsets.length == 4 && offsets[0] == 3 && offsets[1] == 2 && offsets[2] == 1 + && offsets[3] == 0) { + isKMYC = true; + } + } + if (cm.getTransferType() == DataBuffer.TYPE_BYTE + && buffer.getOffset() == 0 + && buffer.getNumBanks() == 1) { + this.firstTileDump = true; + this.encodedColorModel = cm; + } + } + } + + } + + /** + * Encodes the image and writes everything to the given OutputStream. + * @param out the OutputStream + * @throws IOException if an I/O error occurs + */ + public void encode(OutputStream out) throws IOException { + if (!isConverted()) { + if (optimizedWriteTo(out)) { + return; + } + } + writeRGBTo(out); + } + + /** + * Encodes the image's alpha channel. If it doesn't have an alpha channel, an + * {@link IllegalStateException} is thrown. + * @param out the OutputStream + * @throws IOException if an I/O error occurs + */ + public void encodeAlpha(OutputStream out) throws IOException { + if (!hasAlpha()) { + throw new IllegalStateException("Image doesn't have an alpha channel"); + } + Raster alpha = GraphicsUtil.getAlphaRaster(image); + DataBuffer buffer = alpha.getDataBuffer(); + if (buffer instanceof DataBufferByte) { + out.write(((DataBufferByte)buffer).getData()); + } else { + throw new UnsupportedOperationException( + "Alpha raster not supported: " + buffer.getClass().getName()); + } + } + + /** + * Writes all pixels (color components only) of a RenderedImage to an OutputStream. + * @param image the image to be encoded + * @param out the OutputStream to write to + * @throws IOException if an I/O error occurs + */ + public static void encodePackedColorComponents(RenderedImage image, OutputStream out) + throws IOException { + ImageEncodingHelper helper = new ImageEncodingHelper(image); + helper.encode(out); + } + + /** + * Create an ImageEncoder for the given RenderImage instance. + * @param img the image + * @return the requested ImageEncoder + */ + public static ImageEncoder createRenderedImageEncoder(RenderedImage img) { + return new RenderedImageEncoder(img); + } + + /** + * ImageEncoder implementation for RenderedImage instances. + */ + private static class RenderedImageEncoder implements ImageEncoder { + + private final RenderedImage img; + + public RenderedImageEncoder(RenderedImage ri) { + if (ri instanceof BufferedImage && ((BufferedImage) ri).getType() == BufferedImage.TYPE_4BYTE_ABGR) { + BufferedImage convertedImg = + new BufferedImage(ri.getWidth(), ri.getHeight(), BufferedImage.TYPE_INT_RGB); + Graphics2D g = (Graphics2D) convertedImg.getGraphics(); + g.setBackground(Color.WHITE); + g.clearRect(0, 0, ri.getWidth(), ri.getHeight()); + g.drawImage((BufferedImage)ri, 0, 0, null); + g.dispose(); + ri = convertedImg; + } + img = ri; + } + + public void writeTo(OutputStream out) throws IOException { + ImageEncodingHelper.encodePackedColorComponents(img, out); + } + + public String getImplicitFilter() { + return null; //No implicit filters with RenderedImage instances + } + } + + public void setBWInvert(boolean v) { + bwinvert = v; + } +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/ImageFormGenerator.java b/src/main/java/org/apache/xmlgraphics/ps/ImageFormGenerator.java new file mode 100644 index 0000000..907b66e --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/ImageFormGenerator.java @@ -0,0 +1,173 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageFormGenerator.java 1881060 2020-08-21 15:38:04Z ssteiner $ */ + +package org.apache.xmlgraphics.ps; + +import java.awt.Dimension; +import java.awt.color.ColorSpace; +import java.awt.geom.AffineTransform; +import java.awt.geom.Dimension2D; +import java.awt.image.RenderedImage; +import java.io.IOException; + +/** + * Abstract helper class for generating PostScript forms. + */ +public class ImageFormGenerator extends FormGenerator { + + //Mode 1 (RenderedImage) + private RenderedImage image; + + //Mode 2 (ImageEncoder) + private ImageEncoder encoder; + private ColorSpace colorSpace; + private int bitsPerComponent = 8; + + private boolean invertImage; + private Dimension pixelDimensions; + + /** + * Main constructor. + * @param formName the form's name + * @param title the form's title or null + * @param dimensions the form's dimensions in units (usually points) + * @param image the image + * @param invertImage true if the image shall be inverted + */ + public ImageFormGenerator(String formName, String title, + Dimension2D dimensions, + RenderedImage image, boolean invertImage) { + super(formName, title, dimensions); + this.image = image; + this.encoder = ImageEncodingHelper.createRenderedImageEncoder(image); + this.invertImage = invertImage; + this.pixelDimensions = new Dimension(image.getWidth(), image.getHeight()); + } + + /** + * Main constructor. + * @param formName the form's name + * @param title the form's title or null + * @param dimensions the form's dimensions in units (usually points) + * @param dimensionsPx the form's dimensions in pixels + * @param encoder the image encoder + * @param colorSpace the target color space + * @param bitsPerComponent the bits per component + * @param invertImage true if the image shall be inverted + */ + public ImageFormGenerator(String formName, String title, + Dimension2D dimensions, Dimension dimensionsPx, + ImageEncoder encoder, + ColorSpace colorSpace, int bitsPerComponent, boolean invertImage) { + super(formName, title, dimensions); + this.pixelDimensions = dimensionsPx; + this.encoder = encoder; + this.colorSpace = colorSpace; + this.bitsPerComponent = bitsPerComponent; + this.invertImage = invertImage; + } + + /** + * Main constructor. + * @param formName the form's name + * @param title the form's title or null + * @param dimensions the form's dimensions in units (usually points) + * @param dimensionsPx the form's dimensions in pixels + * @param encoder the image encoder + * @param colorSpace the target color space + * @param invertImage true if the image shall be inverted + */ + public ImageFormGenerator(String formName, String title, + Dimension2D dimensions, Dimension dimensionsPx, + ImageEncoder encoder, + ColorSpace colorSpace, boolean invertImage) { + this(formName, title, dimensions, dimensionsPx, encoder, colorSpace, 8, invertImage); + } + + /** + * Returns the name of the data segment associated with this image form. + * @return the data segment name + */ + protected String getDataName() { + return getFormName() + ":Data"; + } + + private String getAdditionalFilters(PSGenerator gen) { + String implicitFilter = encoder.getImplicitFilter(); + if (implicitFilter != null) { + return "/ASCII85Decode filter " + implicitFilter + " filter"; + } else { + if (gen.getPSLevel() >= 3) { + return "/ASCII85Decode filter"; + } else { + return "/ASCII85Decode filter /RunLengthDecode filter"; + } + } + } + + /** {@inheritDoc} */ + protected void generatePaintProc(PSGenerator gen) throws IOException { + if (gen.getPSLevel() == 2) { + gen.writeln(" userdict /i 0 put"); //rewind image data + } else { + gen.writeln(" " + getDataName() + " 0 setfileposition"); //rewind image data + } + String dataSource; + if (gen.getPSLevel() == 2) { + dataSource = "{ " + getDataName() + " i get /i i 1 add store } bind"; + } else { + dataSource = getDataName(); + if (gen.getPSLevel() >= 3) { + String implicitFilter = encoder.getImplicitFilter(); + if (implicitFilter == null) { + dataSource += " /FlateDecode filter"; + } + } + } + AffineTransform at = new AffineTransform(); + at.scale(getDimensions().getWidth(), getDimensions().getHeight()); + gen.concatMatrix(at); + PSDictionary imageDict = new PSDictionary(); + imageDict.put("/DataSource", dataSource); + if (this.image != null) { + PSImageUtils.writeImageCommand(this.image, imageDict, gen); + } else { + imageDict.put("/BitsPerComponent", Integer.toString(this.bitsPerComponent)); + PSImageUtils.writeImageCommand(imageDict, + this.pixelDimensions, this.colorSpace, this.invertImage, + gen); + } + } + + /** {@inheritDoc} */ + protected void generateAdditionalDataStream(PSGenerator gen) throws IOException { + gen.writeln("/" + getDataName() + " currentfile"); + gen.writeln(getAdditionalFilters(gen)); + if (gen.getPSLevel() == 2) { + //Creates a data array from the inline file + gen.writeln("{ /temp exch def [" + + " { temp 16384 string readstring not {exit } if } loop ] } exec"); + } else { + gen.writeln("/ReusableStreamDecode filter"); + } + PSImageUtils.compressAndWriteBitmap(encoder, gen); + gen.writeln("def"); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/PSCommandMap.java b/src/main/java/org/apache/xmlgraphics/ps/PSCommandMap.java new file mode 100644 index 0000000..edd516f --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/PSCommandMap.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PSCommandMap.java 924840 2010-03-18 14:52:36Z jeremias $ */ + +package org.apache.xmlgraphics.ps; + +/** + * Interface to map standard PostScript commands to other commands or macros, for example + * shorthands for compact PostScript code. + */ +public interface PSCommandMap { + + /** + * Maps a standard PostScript command (like "setlinejoin" or "setrgbcolor") to a macro. If + * no mapping is available, the command itself is returned again. + * @param command the command + * @return the mapped command (or the "command" parameter if no mapping is available) + */ + String mapCommand(String command); + +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/PSDictionary.java b/src/main/java/org/apache/xmlgraphics/ps/PSDictionary.java new file mode 100644 index 0000000..f5db556 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/PSDictionary.java @@ -0,0 +1,311 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PSDictionary.java 1804124 2017-08-04 14:13:54Z ssteiner $ */ + +package org.apache.xmlgraphics.ps; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.StringTokenizer; + +// CSOFF: InnerAssignment + +/** + * This class is used to encapsulate postscript dictionary objects. + */ +public class PSDictionary extends java.util.HashMap { + + private static final long serialVersionUID = 815367222496219197L; + + /** + * This class is used to parse dictionary strings. + */ + private static class Maker { + + /** + * Simple token holding class + */ + private static class Token { + /** + * start index in string + */ + private int startIndex = -1; + + /** + * end index in string + */ + private int endIndex = -1; + + /** + * token string value + */ + private String value; + } + + private static final String[][] BRACES = { + {"<<", ">>"}, + {"[", "]"}, + {"{", "}"}, + {"(", ")"} + }; + + private static final int OPENING = 0; + private static final int CLOSING = 1; + private static final int DICTIONARY = 0; + private static final int ARRAY = 1; + private static final int PROCEDURE = 2; + private static final int STRING = 3; + + /** + * Returns a Token containing the start, end index and value of the next token + * found in a given string + * + * @param str + * string to search + * @param fromIndex + * search from index + * @return Token containing the start, end index and value of the next token + */ + protected Token nextToken(String str, int fromIndex) { + Token t = null; + for (int i = fromIndex; i < str.length(); i++) { + boolean isWhitespace = Character.isWhitespace(str.charAt(i)); + // start index found + if (t == null && !isWhitespace) { + t = new Token(); + t.startIndex = i; + // end index found + } else if (t != null && isWhitespace) { + t.endIndex = i; + break; + } + } + // start index found + if (t != null) { + // end index not found so take end of string + if (t.endIndex == -1) { + t.endIndex = str.length(); + } + t.value = str.substring(t.startIndex, t.endIndex); + } + return t; + } + + /** + * Returns the closing brace index from a given string searches from a + * given index + * + * @param str + * string to search + * @param braces + * string array of opening and closing brace + * @param fromIndex + * searches from index + * @return matching brace index + * @throws org.apache.xmlgraphics.ps.PSDictionaryFormatException + * thrown in the event that a parsing error occurred + */ + private int indexOfMatchingBrace(String str, String[] braces, + int fromIndex) throws PSDictionaryFormatException { + final int len = str.length(); + if (braces.length != 2) { + throw new PSDictionaryFormatException("Wrong number of braces"); + } + for (int openCnt = 0, closeCnt = 0; fromIndex < len; fromIndex++) { + if (str.startsWith(braces[OPENING], fromIndex)) { + openCnt++; + } else if (str.startsWith(braces[CLOSING], fromIndex)) { + closeCnt++; + if (openCnt > 0 && openCnt == closeCnt) { + return fromIndex; // found + } + } + } + return -1; // not found + } + + /** + * Strips braces from complex object string + * + * @param str + * String to parse + * @param braces + * String array containing opening and closing braces + * @return String with braces stripped + * @throws org.apache.xmlgraphics.ps.PSDictionaryFormatException + * thrown in the event that a parsing error occurred + */ + private String stripBraces(String str, String[] braces) throws PSDictionaryFormatException { + // find first opening brace + int firstIndex = str.indexOf(braces[OPENING]); + if (firstIndex == -1) { + throw new PSDictionaryFormatException( + "Failed to find opening parameter '" + braces[OPENING] + + ""); + } + + // find last matching brace + int lastIndex = indexOfMatchingBrace(str, braces, firstIndex); + if (lastIndex == -1) { + throw new PSDictionaryFormatException( + "Failed to find matching closing parameter '" + + braces[CLOSING] + "'"); + } + + // strip brace and trim + int braceLen = braces[OPENING].length(); + str = str.substring(firstIndex + braceLen, lastIndex).trim(); + return str; + } + + /** + * Parses a dictionary string and provides a dictionary object + * + * @param str a dictionary string + * @return A postscript dictionary object + * @throws org.apache.xmlgraphics.ps.PSDictionaryFormatException + * thrown in the event that a parsing error occurred + */ + public PSDictionary parseDictionary(String str) throws PSDictionaryFormatException { + PSDictionary dictionary = new PSDictionary(); + str = stripBraces(str.trim(), BRACES[DICTIONARY]); + // length of dictionary string + final int len = str.length(); + + Token keyToken; + for (int currIndex = 0; (keyToken = nextToken(str, currIndex)) != null + && currIndex <= len;) { + if (keyToken.value == null) { + throw new PSDictionaryFormatException("Failed to parse object key"); + } + Token valueToken = nextToken(str, keyToken.endIndex + 1); + String[] braces = null; + for (String[] brace : BRACES) { + if (valueToken.value.startsWith(brace[OPENING])) { + braces = brace; + break; + } + } + Object obj = null; + if (braces != null) { + // find closing brace + valueToken.endIndex = indexOfMatchingBrace(str, braces, + valueToken.startIndex) + + braces[OPENING].length(); + if (valueToken.endIndex < 0) { + throw new PSDictionaryFormatException("Closing value brace '" + + braces[CLOSING] + "' not found for key '" + + keyToken.value + "'"); + } + valueToken.value = str.substring(valueToken.startIndex, valueToken.endIndex); + } + if (braces == null || braces == BRACES[PROCEDURE] || braces == BRACES[STRING]) { + obj = valueToken.value; + } else if (BRACES[ARRAY] == braces) { + List objList = new java.util.ArrayList(); + String objString = stripBraces(valueToken.value, braces); + StringTokenizer tokenizer = new StringTokenizer(objString, ","); + while (tokenizer.hasMoreTokens()) { + objList.add(tokenizer.nextToken()); + } + obj = objList; + } else if (BRACES[DICTIONARY] == braces) { + obj = parseDictionary(valueToken.value); + } + dictionary.put(keyToken.value, obj); + currIndex = valueToken.endIndex + 1; + } + return dictionary; + } + } + + /** + * Parses a given a dictionary string and returns an object + * + * @param str dictionary string + * @return dictionary object + * @throws org.apache.xmlgraphics.ps.PSDictionaryFormatException + * thrown in the event that a parsing error occurred + */ + public static PSDictionary valueOf(String str) throws PSDictionaryFormatException { + return (new Maker()).parseDictionary(str); + } + + /** + * @param obj object to test equality against + * @return whether a given object is equal to this dictionary object + * @see java.lang.Object#equals(Object) + */ + public boolean equals(Object obj) { + if (!(obj instanceof PSDictionary)) { + return false; + } + PSDictionary dictionaryObj = (PSDictionary) obj; + if (dictionaryObj.size() != size()) { + return false; + } + for (Object e : entrySet()) { + Map.Entry entry = (Map.Entry) e; + String key = (String) entry.getKey(); + if (!dictionaryObj.containsKey(key)) { + return false; + } + if (!dictionaryObj.get(key).equals(entry.getValue())) { + return false; + } + } + return true; + } + + /** {@inheritDoc} */ + public int hashCode() { + int hashCode = 7; + for (Object value : values()) { + hashCode += value.hashCode(); + } + return hashCode; + } + + /** {@inheritDoc} */ + public String toString() { + if (isEmpty()) { + return ""; + } + StringBuffer sb = new StringBuffer("<<\n"); + for (Object o : super.keySet()) { + String key = (String) o; + sb.append(" " + key + " "); + Object obj = super.get(key); + if (obj instanceof ArrayList) { + List array = (List) obj; + StringBuilder str = new StringBuilder("["); + for (Object element : array) { + str.append(element + " "); + } + String str2 = str.toString().trim(); + str2 += "]"; + sb.append(str2 + "\n"); + } else { + sb.append(obj.toString() + "\n"); + } + } + sb.append(">>"); + return sb.toString(); + } +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/PSDictionaryFormatException.java b/src/main/java/org/apache/xmlgraphics/ps/PSDictionaryFormatException.java new file mode 100644 index 0000000..17f0534 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/PSDictionaryFormatException.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PSDictionaryFormatException.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.ps; + +/** + * Thrown to indicate that a formatting error has occurred when + * trying to parse a PostScript dictionary object + */ +public class PSDictionaryFormatException extends Exception { + + private static final long serialVersionUID = 6492321557297860931L; + + /** + * Default constructor + * @param string error message + */ + public PSDictionaryFormatException(String string) { + super(string); + } +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/PSFontUtils.java b/src/main/java/org/apache/xmlgraphics/ps/PSFontUtils.java new file mode 100644 index 0000000..3a7039b --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/PSFontUtils.java @@ -0,0 +1,182 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PSFontUtils.java 1345759 2012-06-03 20:09:09Z gadams $ */ + +package org.apache.xmlgraphics.ps; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; + +import org.apache.commons.io.EndianUtils; +import org.apache.commons.io.IOUtils; + +import org.apache.xmlgraphics.fonts.Glyphs; +import org.apache.xmlgraphics.util.io.ASCIIHexOutputStream; +import org.apache.xmlgraphics.util.io.SubInputStream; + +// CSOFF: HideUtilityClassConstructor + +/** + * Utility code for font handling in PostScript. + */ +public class PSFontUtils { + + public PSFontUtils() { + } + + /** + * This method reads a Type 1 font from a stream and embeds it into a PostScript stream. + * Note: Only the IBM PC Format as described in section 3.3 of the Adobe Technical Note #5040 + * is supported. + * @param gen The PostScript generator + * @param in the InputStream from which to read the Type 1 font + * @throws IOException in case an I/O problem occurs + */ + public static void embedType1Font(PSGenerator gen, InputStream in) throws IOException { + boolean finished = false; + while (!finished) { + int segIndicator = in.read(); + if (segIndicator < 0) { + throw new IOException("Unexpected end-of-file while reading segment indicator"); + } else if (segIndicator != 128) { + throw new IOException("Expected ASCII 128, found: " + segIndicator); + } + int segType = in.read(); + if (segType < 0) { + throw new IOException("Unexpected end-of-file while reading segment type"); + } + int dataSegLen = 0; + switch (segType) { + case 1: //ASCII + dataSegLen = EndianUtils.readSwappedInteger(in); + + BufferedReader reader = new BufferedReader( + new java.io.InputStreamReader( + new SubInputStream(in, dataSegLen), "US-ASCII")); + String line; + while ((line = reader.readLine()) != null) { + gen.writeln(line); + } + break; + case 2: //binary + dataSegLen = EndianUtils.readSwappedInteger(in); + + SubInputStream sin = new SubInputStream(in, dataSegLen); + ASCIIHexOutputStream hexOut = new ASCIIHexOutputStream(gen.getOutputStream()); + IOUtils.copy(sin, hexOut); + gen.newLine(); + break; + case 3: //EOF + finished = true; + break; + default: throw new IOException("Unsupported segment type: " + segType); + } + } + } + + /** the PSResource representing the WinAnsiEncoding. */ + public static final PSResource WINANSI_ENCODING_RESOURCE + = new PSResource(PSResource.TYPE_ENCODING, "WinAnsiEncoding"); + + /** + * Defines the WinAnsi encoding for use in PostScript files. + * @param gen the PostScript generator + * @throws IOException In case of an I/O problem + */ + public static void defineWinAnsiEncoding(PSGenerator gen) throws IOException { + gen.writeDSCComment(DSCConstants.BEGIN_RESOURCE, WINANSI_ENCODING_RESOURCE); + gen.writeln("/WinAnsiEncoding ["); + for (int i = 0; i < Glyphs.WINANSI_ENCODING.length; i++) { + if (i > 0) { + if ((i % 5) == 0) { + gen.newLine(); + } else { + gen.write(" "); + } + } + final char ch = Glyphs.WINANSI_ENCODING[i]; + final String glyphname = Glyphs.charToGlyphName(ch); + if ("".equals(glyphname)) { + gen.write("/" + Glyphs.NOTDEF); + } else { + gen.write("/"); + gen.write(glyphname); + } + } + gen.newLine(); + gen.writeln("] def"); + gen.writeDSCComment(DSCConstants.END_RESOURCE); + gen.getResourceTracker().registerSuppliedResource(WINANSI_ENCODING_RESOURCE); + } + + /** the PSResource representing the AdobeStandardCyrillicEncoding. */ + public static final PSResource ADOBECYRILLIC_ENCODING_RESOURCE + = new PSResource(PSResource.TYPE_ENCODING, "AdobeStandardCyrillicEncoding"); + + /** + * Defines the AdobeStandardCyrillic encoding for use in PostScript files. + * @param gen the PostScript generator + * @throws IOException In case of an I/O problem + */ + public static void defineAdobeCyrillicEncoding(PSGenerator gen) throws IOException { + gen.writeDSCComment(DSCConstants.BEGIN_RESOURCE, ADOBECYRILLIC_ENCODING_RESOURCE); + gen.writeln("/AdobeStandardCyrillicEncoding ["); + for (int i = 0; i < Glyphs.ADOBECYRILLIC_ENCODING.length; i++) { + if (i > 0) { + if ((i % 5) == 0) { + gen.newLine(); + } else { + gen.write(" "); + } + } + final char ch = Glyphs.ADOBECYRILLIC_ENCODING[i]; + final String glyphname = Glyphs.charToGlyphName(ch); + if ("".equals(glyphname)) { + gen.write("/" + Glyphs.NOTDEF); + } else { + gen.write("/"); + gen.write(glyphname); + } + } + gen.newLine(); + gen.writeln("] def"); + gen.writeDSCComment(DSCConstants.END_RESOURCE); + gen.getResourceTracker().registerSuppliedResource(ADOBECYRILLIC_ENCODING_RESOURCE); + } + + + /** + * Redefines the encoding of a font. + * @param gen the PostScript generator + * @param fontName the font name + * @param encoding the new encoding (must be predefined in the PS file) + * @throws IOException In case of an I/O problem + */ + public static void redefineFontEncoding(PSGenerator gen, String fontName, String encoding) + throws IOException { + gen.writeln("/" + fontName + " findfont"); + gen.writeln("dup length dict begin"); + gen.writeln(" {1 index /FID ne {def} {pop pop} ifelse} forall"); + gen.writeln(" /Encoding " + encoding + " def"); + gen.writeln(" currentdict"); + gen.writeln("end"); + gen.writeln("/" + fontName + " exch definefont pop"); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/PSGenerator.java b/src/main/java/org/apache/xmlgraphics/ps/PSGenerator.java new file mode 100644 index 0000000..900d9d5 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/PSGenerator.java @@ -0,0 +1,893 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PSGenerator.java 1809627 2017-09-25 13:42:08Z ssteiner $ */ + +package org.apache.xmlgraphics.ps; + +import java.awt.Color; +import java.awt.color.ColorSpace; +import java.awt.geom.AffineTransform; +import java.awt.geom.Rectangle2D; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.text.DateFormat; +import java.util.Date; +import java.util.Stack; + +import javax.xml.transform.Source; + +import org.apache.commons.io.IOUtils; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.apache.xmlgraphics.java2d.color.ColorUtil; +import org.apache.xmlgraphics.java2d.color.ColorWithAlternatives; +import org.apache.xmlgraphics.ps.dsc.ResourceTracker; +import org.apache.xmlgraphics.util.DoubleFormatUtil; + +/** + * This class is used to output PostScript code to an OutputStream. This class assumes that + * the {@link PSProcSets#STD_PROCSET} has been added to the PostScript file. + * + * @version $Id: PSGenerator.java 1809627 2017-09-25 13:42:08Z ssteiner $ + */ +public class PSGenerator implements PSCommandMap { + + /** + * Default postscript language level + */ + public static final int DEFAULT_LANGUAGE_LEVEL = 3; + + /** + * Indicator for the PostScript interpreter that the value is provided + * later in the document (mostly in the %%Trailer section). + * @deprecated Please use DSCConstants.ATEND. This constant was in the wrong place. + */ + @Deprecated + public static final Object ATEND = DSCConstants.ATEND; + + /** Line feed used by PostScript */ + public static final char LF = '\n'; + + private static final String IDENTITY_H = "Identity-H"; + + private Log log = LogFactory.getLog(getClass()); + private OutputStream out; + private int psLevel = DEFAULT_LANGUAGE_LEVEL; + private boolean acrobatDownsample; + private boolean commentsEnabled = true; + private boolean compactMode = true; + private PSCommandMap commandMap = PSProcSets.STD_COMMAND_MAP; + + private Stack graphicsStateStack = new Stack(); + private PSState currentState; + + private StringBuffer doubleBuffer = new StringBuffer(16); + + private StringBuffer tempBuffer = new StringBuffer(256); + + private boolean identityHEmbedded; + + private PSResource procsetCIDInitResource; + + private PSResource identityHCMapResource; + + /** + * Creates a new instance. + * @param out the OutputStream to write the generated PostScript code to + */ + public PSGenerator(OutputStream out) { + this.out = out; + resetGraphicsState(); + } + + /** + * Indicates whether this instance is in compact mode. See {@link #setCompactMode(boolean)} + * for details. + * @return true if compact mode is enabled (the default) + */ + public boolean isCompactMode() { + return this.compactMode; + } + + /** + * Controls whether this instance shall produce compact PostScript (omitting comments and + * using short macros). Enabling this mode requires that the standard procset + * ({@link PSProcSets#STD_PROCSET}) is included in the PostScript file. Setting this to + * false produces more verbose PostScript suitable for debugging. + * @param value true to enable compact mode, false for verbose mode + */ + public void setCompactMode(boolean value) { + this.compactMode = value; + } + + /** + * Indicates whether this instance allows to write comments. See + * {@link #setCommentsEnabled(boolean)} for details. + * @return true if comments are enabled (the default) + */ + public boolean isCommentsEnabled() { + return this.commentsEnabled; + } + + /** + * Controls whether this instance allows to write comments using the {@link #commentln(String)} + * method. + * @param value true to enable comments, false to disable them + */ + public void setCommentsEnabled(boolean value) { + this.commentsEnabled = value; + } + + private void resetGraphicsState() { + if (!this.graphicsStateStack.isEmpty()) { + throw new IllegalStateException("Graphics state stack should be empty at this point"); + } + this.currentState = new PSState(); + } + + /** + * Returns the OutputStream the PSGenerator writes to. + * @return the OutputStream + */ + public OutputStream getOutputStream() { + return this.out; + } + + /** + * Returns the selected PostScript level. + * @return the PostScript level + */ + public int getPSLevel() { + return this.psLevel; + } + + /** + * Sets the PostScript level that is used to generate the current document. + * @param level the PostScript level (currently 1, 2 and 3 are known) + */ + public void setPSLevel(int level) { + this.psLevel = level; + } + + public boolean isAcrobatDownsample() { + return acrobatDownsample; + } + + public void setAcrobatDownsample(boolean b) { + acrobatDownsample = b; + } + + /** + * Attempts to resolve the given URI. PSGenerator should be subclasses to provide more + * sophisticated URI resolution. + * @param uri URI to access + * @return A {@link javax.xml.transform.Source} object, or null if the URI + * cannot be resolved. + */ + public Source resolveURI(String uri) { + return new javax.xml.transform.stream.StreamSource(uri); + } + + /** + * Writes a newline character to the OutputStream. + * + * @throws IOException In case of an I/O problem + */ + public final void newLine() throws IOException { + out.write(LF); + } + + /** + * Formats a double value for PostScript output. + * + * @param value value to format + * @return the formatted value + */ + public String formatDouble(double value) { + doubleBuffer.setLength(0); + DoubleFormatUtil.formatDouble(value, 3, 3, doubleBuffer); + return doubleBuffer.toString(); + } + + /** + * Formats a double value for PostScript output (higher resolution). + * + * @param value value to format + * @return the formatted value + */ + public String formatDouble5(double value) { + doubleBuffer.setLength(0); + DoubleFormatUtil.formatDouble(value, 5, 5, doubleBuffer); + return doubleBuffer.toString(); + } + + /** + * Writes a PostScript command to the stream. + * + * @param cmd The PostScript code to be written. + * @exception IOException In case of an I/O problem + */ + public void write(String cmd) throws IOException { + /* TODO Check disabled until clarification. + if (cmd.length() > 255) { + throw new RuntimeException("PostScript command exceeded limit of 255 characters"); + } */ + out.write(cmd.getBytes("US-ASCII")); + } + + /** + * Writes the given number to the stream in decimal format. + * + * @param n a number + * @throws IOException in case of an I/O problem + */ + public void write(int n) throws IOException { + write(Integer.toString(n)); + } + + /** + * Writes a PostScript command to the stream and ends the line. + * + * @param cmd The PostScript code to be written. + * @exception IOException In case of an I/O problem + */ + public void writeln(String cmd) throws IOException { + write(cmd); + newLine(); + } + + /** + * Writes a comment to the stream and ends the line. Output of comments can + * be disabled to reduce the size of the generated file. + * + * @param comment comment to write + * @exception IOException In case of an I/O problem + */ + public void commentln(String comment) throws IOException { + if (isCommentsEnabled()) { + writeln(comment); + } + } + + /** {@inheritDoc} */ + public String mapCommand(String command) { + if (isCompactMode()) { + return this.commandMap.mapCommand(command); + } else { + return command; + } + } + + /** + * Writes encoded data to the PostScript stream. + * + * @param cmd The encoded PostScript code to be written. + * @exception IOException In case of an I/O problem + */ + public void writeByteArr(byte[] cmd) throws IOException { + out.write(cmd); + newLine(); + } + + + /** + * Flushes the OutputStream. + * + * @exception IOException In case of an I/O problem + */ + public void flush() throws IOException { + out.flush(); + } + + /** + * Escapes a character conforming to the rules established in the PostScript + * Language Reference (Search for "Literal Text Strings"). + * @param c character to escape + * @param target target StringBuffer to write the escaped character to + */ + public static final void escapeChar(char c, StringBuffer target) { + switch (c) { + case '\n': + target.append("\\n"); + break; + case '\r': + target.append("\\r"); + break; + case '\t': + target.append("\\t"); + break; + case '\b': + target.append("\\b"); + break; + case '\f': + target.append("\\f"); + break; + case '\\': + target.append("\\\\"); + break; + case '(': + target.append("\\("); + break; + case ')': + target.append("\\)"); + break; + default: + if (c > 255) { + //Ignoring non Latin-1 characters + target.append('?'); + } else if (c < 32 || c > 127) { + target.append('\\'); + + target.append((char)('0' + (c >> 6))); + target.append((char)('0' + ((c >> 3) % 8))); + target.append((char)('0' + (c % 8))); + //Integer.toOctalString(i) + } else { + target.append(c); + } + } + } + + + /** + * Converts text by applying escaping rules established in the DSC specs. + * @param text Text to convert + * @return String The resulting String + */ + public static final String convertStringToDSC(String text) { + return convertStringToDSC(text, false); + } + + /** + * Converts a <real> value for use in DSC comments. + * @param value the value to convert + * @return String The resulting String + */ + public static final String convertRealToDSC(float value) { + return Float.toString(value); + } + + /** + * Converts text by applying escaping rules established in the DSC specs. + * @param text Text to convert + * @param forceParentheses Force the use of parentheses + * @return String The resulting String + */ + public static final String convertStringToDSC(String text, + boolean forceParentheses) { + if ((text == null) || (text.length() == 0)) { + return "()"; + } else { + int initialSize = text.length(); + initialSize += initialSize / 2; + StringBuffer sb = new StringBuffer(initialSize); + if ((text.indexOf(' ') >= 0) || forceParentheses) { + sb.append('('); + for (int i = 0; i < text.length(); i++) { + final char c = text.charAt(i); + escapeChar(c, sb); + } + sb.append(')'); + return sb.toString(); + } else { + for (int i = 0; i < text.length(); i++) { + final char c = text.charAt(i); + escapeChar(c, sb); + } + return sb.toString(); + } + } + } + + + /** + * Writes a DSC comment to the output stream. + * @param name Name of the DSC comment + * @exception IOException In case of an I/O problem + * @see org.apache.xmlgraphics.ps.DSCConstants + */ + public void writeDSCComment(String name) throws IOException { + writeln("%%" + name); + } + + + /** + * Writes a DSC comment to the output stream. The parameter to the DSC + * comment can be any object. The object is converted to a String as + * necessary. + * @param name Name of the DSC comment + * @param param Single parameter to the DSC comment + * @exception IOException In case of an I/O problem + * @see org.apache.xmlgraphics.ps.DSCConstants + */ + public void writeDSCComment(String name, Object param) throws IOException { + writeDSCComment(name, new Object[] {param}); + } + + + /** + * Writes a DSC comment to the output stream. The parameters to the DSC + * comment can be any object. The objects are converted to Strings as + * necessary. Please see the source code to find out what parameters are + * currently supported. + * @param name Name of the DSC comment + * @param params Array of parameters to the DSC comment + * @exception IOException In case of an I/O problem + * @see org.apache.xmlgraphics.ps.DSCConstants + */ + public void writeDSCComment(String name, Object[] params) throws IOException { + tempBuffer.setLength(0); + tempBuffer.append("%%"); + tempBuffer.append(name); + if ((params != null) && (params.length > 0)) { + tempBuffer.append(": "); + for (int i = 0; i < params.length; i++) { + if (i > 0) { + tempBuffer.append(" "); + } + + if (params[i] instanceof String) { + tempBuffer.append(convertStringToDSC((String)params[i])); + } else if (params[i] == DSCConstants.ATEND) { + tempBuffer.append(DSCConstants.ATEND); + } else if (params[i] instanceof Double) { + tempBuffer.append(formatDouble((Double) params[i])); + } else if (params[i] instanceof Number) { + tempBuffer.append(params[i].toString()); + } else if (params[i] instanceof Date) { + DateFormat datef = new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); + tempBuffer.append(convertStringToDSC(datef.format((Date)params[i]))); + } else if (params[i] instanceof PSResource) { + tempBuffer.append(((PSResource)params[i]).getResourceSpecification()); + } else { + throw new IllegalArgumentException("Unsupported parameter type: " + + params[i].getClass().getName()); + } + } + } + writeln(tempBuffer.toString()); + } + + + /** + * Saves the graphics state of the rendering engine. + * @exception IOException In case of an I/O problem + */ + public void saveGraphicsState() throws IOException { + writeln(mapCommand("gsave")); + + PSState state = new PSState(this.currentState, false); + this.graphicsStateStack.push(this.currentState); + this.currentState = state; + } + + /** + * Restores the last graphics state of the rendering engine. + * @return true if the state was restored, false if there's a stack underflow. + * @exception IOException In case of an I/O problem + */ + public boolean restoreGraphicsState() throws IOException { + if (this.graphicsStateStack.size() > 0) { + writeln(mapCommand("grestore")); + this.currentState = this.graphicsStateStack.pop(); + return true; + } else { + return false; + } + } + + + /** + * Returns the current graphics state. + * @return the current graphics state + */ + public PSState getCurrentState() { + return this.currentState; + } + + /** + * Issues the "showpage" command and resets the painting state accordingly. + * @exception IOException In case of an I/O problem + */ + public void showPage() throws IOException { + writeln("showpage"); + resetGraphicsState(); + } + + /** + * Concats the transformation matrix. + * @param a A part + * @param b B part + * @param c C part + * @param d D part + * @param e E part + * @param f F part + * @exception IOException In case of an I/O problem + */ + public void concatMatrix(double a, double b, + double c, double d, + double e, double f) throws IOException { + AffineTransform at = new AffineTransform(a, b, c, d, e, f); + concatMatrix(at); + + } + + /** + * Concats the transformations matrix. + * @param matrix Matrix to use + * @exception IOException In case of an I/O problem + */ + public void concatMatrix(double[] matrix) throws IOException { + concatMatrix(matrix[0], matrix[1], + matrix[2], matrix[3], + matrix[4], matrix[5]); + } + + /** + * Formats a transformation matrix. + * @param at the AffineTransform with the matrix + * @return the formatted transformation matrix (example: "[1 0 0 1 0 0]") + */ + public String formatMatrix(AffineTransform at) { + double[] matrix = new double[6]; + at.getMatrix(matrix); + return "[" + formatDouble5(matrix[0]) + " " + + formatDouble5(matrix[1]) + " " + + formatDouble5(matrix[2]) + " " + + formatDouble5(matrix[3]) + " " + + formatDouble5(matrix[4]) + " " + + formatDouble5(matrix[5]) + "]"; + } + + /** + * Concats the transformations matric. + * @param at the AffineTransform whose matrix to use + * @exception IOException In case of an I/O problem + */ + public void concatMatrix(AffineTransform at) throws IOException { + getCurrentState().concatMatrix(at); + writeln(formatMatrix(at) + " " + mapCommand("concat")); + } + + /** + * Formats a Rectangle2D to an array. + * @param rect the rectangle + * @return the formatted array + */ + public String formatRectangleToArray(Rectangle2D rect) { + return "[" + formatDouble(rect.getX()) + " " + + formatDouble(rect.getY()) + " " + + formatDouble(rect.getWidth()) + " " + + formatDouble(rect.getHeight()) + "]"; + } + + /** + * Adds a rectangle to the current path. + * @param x upper left corner + * @param y upper left corner + * @param w width + * @param h height + * @exception IOException In case of an I/O problem + */ + public void defineRect(double x, double y, double w, double h) + throws IOException { + writeln(formatDouble(x) + + " " + formatDouble(y) + + " " + formatDouble(w) + + " " + formatDouble(h) + + " re"); + } + + /** + * Establishes the specified line cap style. + * @param linecap the line cap style (0, 1 or 2) as defined by the setlinecap command. + * @exception IOException In case of an I/O problem + */ + public void useLineCap(int linecap) throws IOException { + if (getCurrentState().useLineCap(linecap)) { + writeln(linecap + " " + mapCommand("setlinecap")); + } + } + + /** + * Establishes the specified line join style. + * @param linejoin the line join style (0, 1 or 2) as defined by the setlinejoin command. + * @exception IOException In case of an I/O problem + */ + public void useLineJoin(int linejoin) throws IOException { + if (getCurrentState().useLineJoin(linejoin)) { + writeln(linejoin + " " + mapCommand("setlinejoin")); + } + } + + /** + * Establishes the specified miter limit. + * @param miterlimit the miter limit as defined by the setmiterlimit command. + * @exception IOException In case of an I/O problem + */ + public void useMiterLimit(float miterlimit) throws IOException { + if (getCurrentState().useMiterLimit(miterlimit)) { + writeln(miterlimit + " " + mapCommand("setmiterlimit")); + } + } + + /** + * Establishes the specified line width. + * @param width the line width as defined by the setlinewidth command. + * @exception IOException In case of an I/O problem + */ + public void useLineWidth(double width) throws IOException { + if (getCurrentState().useLineWidth(width)) { + writeln(formatDouble(width) + " " + mapCommand("setlinewidth")); + } + } + + /** + * Establishes the specified dash pattern. + * @param pattern the dash pattern as defined by the setdash command. + * @exception IOException In case of an I/O problem + */ + public void useDash(String pattern) throws IOException { + if (pattern == null) { + pattern = PSState.DEFAULT_DASH; + } + if (getCurrentState().useDash(pattern)) { + writeln(pattern + " " + mapCommand("setdash")); + } + } + + /** + * Establishes the specified color (RGB). + * @param col the color as defined by the setrgbcolor command. + * @exception IOException In case of an I/O problem + * @deprecated use useColor method instead + */ + @Deprecated + public void useRGBColor(Color col) throws IOException { + useColor(col); + } + + /** + * Establishes the specified color. + * @param col the color. + * @exception IOException In case of an I/O problem + */ + public void useColor(Color col) throws IOException { + if (getCurrentState().useColor(col)) { + writeln(convertColorToPS(col)); + } + } + + private String convertColorToPS(Color color) { + StringBuffer codeBuffer = new StringBuffer(); + + //Important: Right now, CMYK colors are treated as device colors (DeviceCMYK) irrespective + //of any associated color profile. All other colors are converted to sRGB (if necessary) + //and the resulting RGB components are treated as DeviceRGB colors. + //If all three RGB components are the same, DeviceGray is used. + + boolean established = false; + if (color instanceof ColorWithAlternatives) { + ColorWithAlternatives colExt = (ColorWithAlternatives)color; + //Alternative colors have priority + Color[] alt = colExt.getAlternativeColors(); + for (Color col : alt) { + established = establishColorFromColor(codeBuffer, col); + if (established) { + break; + } + } + if (log.isDebugEnabled() && alt.length > 0) { + log.debug("None of the alternative colors are supported. Using fallback: " + + color); + } + } + + //Fallback + if (!established) { + established = establishColorFromColor(codeBuffer, color); + } + if (!established) { + establishFallbackRGB(codeBuffer, color); + } + + return codeBuffer.toString(); + } + + private boolean establishColorFromColor(StringBuffer codeBuffer, Color color) { + //Important: see above note about color handling! + float[] comps = color.getColorComponents(null); + if (color.getColorSpace().getType() == ColorSpace.TYPE_CMYK) { + // colorspace is CMYK + writeSetColor(codeBuffer, comps, "setcmykcolor"); + return true; + } + return false; + } + + private void writeSetColor(StringBuffer codeBuffer, float[] comps, String command) { + for (int i = 0, c = comps.length; i < c; i++) { + if (i > 0) { + codeBuffer.append(" "); + } + codeBuffer.append(formatDouble(comps[i])); + } + codeBuffer.append(" ").append(mapCommand(command)); + } + + private void establishFallbackRGB(StringBuffer codeBuffer, Color color) { + float[] comps; + if (color.getColorSpace().isCS_sRGB()) { + comps = color.getColorComponents(null); + } else { + if (log.isDebugEnabled()) { + log.debug("Converting color to sRGB as a fallback: " + color); + } + ColorSpace sRGB = ColorSpace.getInstance(ColorSpace.CS_sRGB); + comps = color.getColorComponents(sRGB, null); + } + assert comps.length == 3; + boolean gray = ColorUtil.isGray(color); + if (gray) { + comps = new float[] {comps[0]}; + } + writeSetColor(codeBuffer, comps, gray ? "setgray" : "setrgbcolor"); + } + + /** + * Establishes the specified font and size. + * @param name name of the font for the "F" command (see FOP Std Proc Set) + * @param size size of the font + * @exception IOException In case of an I/O problem + */ + public void useFont(String name, float size) throws IOException { + if (getCurrentState().useFont(name, size)) { + writeln(name + " " + formatDouble(size) + " F"); + } + } + + private ResourceTracker resTracker = new ResourceTracker(); + + /** + * Resturns the ResourceTracker instance associated with this PSGenerator. + * @return the ResourceTracker instance or null if none is assigned + */ + public ResourceTracker getResourceTracker() { + return this.resTracker; + } + + /** + * Sets the ResourceTracker instance to be associated with this PSGenerator. + * @param resTracker the ResourceTracker instance + */ + public void setResourceTracker(ResourceTracker resTracker) { + this.resTracker = resTracker; + } + + /** + * Notifies the generator that a new page has been started and that the page resource + * set can be cleared. + * @deprecated Use the notifyStartNewPage() on ResourceTracker instead. + */ + @Deprecated + public void notifyStartNewPage() { + getResourceTracker().notifyStartNewPage(); + } + + /** + * Notifies the generator about the usage of a resource on the current page. + * @param res the resource being used + * @param needed true if this is a needed resource, false for a supplied resource + * @deprecated Use the notifyResourceUsageOnPage() on ResourceTracker instead + */ + @Deprecated + public void notifyResourceUsage(PSResource res, boolean needed) { + getResourceTracker().notifyResourceUsageOnPage(res); + } + + /** + * Writes a DSC comment for the accumulated used resources, either at page level or + * at document level. + * @param pageLevel true if the DSC comment for the page level should be generated, + * false for the document level (in the trailer) + * @exception IOException In case of an I/O problem + * @deprecated Use the writeResources() on ResourceTracker instead. + */ + @Deprecated + public void writeResources(boolean pageLevel) throws IOException { + getResourceTracker().writeResources(pageLevel, this); + } + + /** + * Indicates whether a particular resource is supplied, rather than needed. + * @param res the resource + * @return true if the resource is registered as being supplied. + * @deprecated Use the isResourceSupplied() on ResourceTracker instead. + */ + @Deprecated + public boolean isResourceSupplied(PSResource res) { + return getResourceTracker().isResourceSupplied(res); + } + + /** + * Embeds the Identity-H CMap file into the output stream, if that has not + * already been done. + * + * @return true if embedding has actually been performed, false otherwise + * (which means that a call to this method had already been made earlier) + * @throws IOException in case of an I/O problem + */ + public boolean embedIdentityH() throws IOException { + if (identityHEmbedded) { + return false; + } else { + resTracker.registerNeededResource(getProcsetCIDInitResource()); + writeDSCComment(DSCConstants.BEGIN_DOCUMENT, IDENTITY_H); + InputStream cmap = PSGenerator.class.getResourceAsStream(IDENTITY_H); + try { + IOUtils.copyLarge(cmap, out); + } finally { + IOUtils.closeQuietly(cmap); + } + writeDSCComment(DSCConstants.END_DOCUMENT); + resTracker.registerSuppliedResource(getIdentityHCMapResource()); + identityHEmbedded = true; + return true; + } + } + + /** + * Returns the PSResource instance corresponding to the Identity-H CMap + * resource. + * + * @return the Identity-H CMap resource. + */ + public PSResource getIdentityHCMapResource() { + if (identityHCMapResource == null) { + identityHCMapResource = new PSResource(PSResource.TYPE_CMAP, IDENTITY_H); + } + return identityHCMapResource; + } + + /** + * Returns the PSResource instance corresponding to the CIDInit ProcSet + * resource. + * + * @return the ProcSet CIDInit resource + */ + public PSResource getProcsetCIDInitResource() { + if (procsetCIDInitResource == null) { + procsetCIDInitResource = new PSResource(PSResource.TYPE_PROCSET, "CIDInit"); + } + return procsetCIDInitResource; + } + + /** + * Adds a PostScript DSC comment to the output stream requiring the + * inclusion of the CIDInit ProcSet resource. + * + * @throws IOException in case of an I/O problem + */ + public void includeProcsetCIDInitResource() throws IOException { + writeDSCComment(DSCConstants.INCLUDE_RESOURCE, getProcsetCIDInitResource()); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/PSImageUtils.java b/src/main/java/org/apache/xmlgraphics/ps/PSImageUtils.java new file mode 100644 index 0000000..de04da4 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/PSImageUtils.java @@ -0,0 +1,841 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PSImageUtils.java 1792872 2017-04-27 12:30:09Z ssteiner $ */ + +package org.apache.xmlgraphics.ps; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.color.ColorSpace; +import java.awt.geom.Dimension2D; +import java.awt.geom.Rectangle2D; +import java.awt.image.ColorModel; +import java.awt.image.DataBuffer; +import java.awt.image.DataBufferByte; +import java.awt.image.IndexColorModel; +import java.awt.image.Raster; +import java.awt.image.RenderedImage; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Arrays; + +import org.apache.commons.io.IOUtils; + +import org.apache.xmlgraphics.util.io.ASCII85OutputStream; +import org.apache.xmlgraphics.util.io.Finalizable; +import org.apache.xmlgraphics.util.io.FlateEncodeOutputStream; +import org.apache.xmlgraphics.util.io.RunLengthEncodeOutputStream; + +// CSOFF: HideUtilityClassConstructor + +/** + * Utility code for rendering images in PostScript. + */ +public class PSImageUtils { + + public PSImageUtils() { + } + + /** + * Writes a bitmap image to the PostScript stream. + * @param img the bitmap image as a byte array + * @param imgDim the dimensions of the image + * @param imgDescription the name of the image + * @param targetRect the target rectangle to place the image in + * @param isJPEG true if "img" contains a DCT-encoded images, false if "img" contains the + * decoded bitmap + * @param colorSpace the color space of the image + * @param gen the PostScript generator + * @throws IOException In case of an I/O exception + * @deprecated Please use the variant with the more versatile ImageEncoder as parameter + */ + public static void writeImage(final byte[] img, + Dimension imgDim, String imgDescription, + Rectangle2D targetRect, + final boolean isJPEG, ColorSpace colorSpace, + PSGenerator gen) throws IOException { + ImageEncoder encoder = new ImageEncoder() { + public void writeTo(OutputStream out) throws IOException { + out.write(img); + } + + public String getImplicitFilter() { + if (isJPEG) { + return "<< >> /DCTDecode"; + } else { + return null; + } + } + }; + writeImage(encoder, imgDim, imgDescription, targetRect, colorSpace, 8, false, gen); + } + + /** + * Writes a bitmap image to the PostScript stream. + * @param encoder the image encoder + * @param imgDim the dimensions of the image + * @param imgDescription the name of the image + * @param targetRect the target rectangle to place the image in + * @param colorSpace the color space of the image + * @param bitsPerComponent the number of bits per component + * @param invertImage true if the image shall be inverted + * @param gen the PostScript generator + * @throws IOException In case of an I/O exception + */ + public static void writeImage(ImageEncoder encoder, + Dimension imgDim, String imgDescription, + Rectangle2D targetRect, + ColorSpace colorSpace, int bitsPerComponent, boolean invertImage, + PSGenerator gen) throws IOException { + gen.saveGraphicsState(); + translateAndScale(gen, null, targetRect); + + gen.commentln("%AXGBeginBitmap: " + imgDescription); + + gen.writeln("{{"); + // Template: (RawData is used for the EOF signal only) + // gen.write("/RawData currentfile filter def"); + // gen.write("/Data RawData [...] def"); + String implicitFilter = encoder.getImplicitFilter(); + if (implicitFilter != null) { + gen.writeln("/RawData currentfile /ASCII85Decode filter def"); + gen.writeln("/Data RawData " + implicitFilter + " filter def"); + } else { + if (gen.getPSLevel() >= 3) { + gen.writeln("/RawData currentfile /ASCII85Decode filter def"); + gen.writeln("/Data RawData /FlateDecode filter def"); + } else { + gen.writeln("/RawData currentfile /ASCII85Decode filter def"); + gen.writeln("/Data RawData /RunLengthDecode filter def"); + } + } + PSDictionary imageDict = new PSDictionary(); + imageDict.put("/DataSource", "Data"); + imageDict.put("/BitsPerComponent", Integer.toString(bitsPerComponent)); + writeImageCommand(imageDict, imgDim, colorSpace, invertImage, gen); + /* the following two lines could be enabled if something still goes wrong + * gen.write("Data closefile"); + * gen.write("RawData flushfile"); + */ + gen.writeln("} stopped {handleerror} if"); + gen.writeln(" RawData flushfile"); + gen.writeln("} exec"); + + compressAndWriteBitmap(encoder, gen); + + gen.newLine(); + gen.commentln("%AXGEndBitmap"); + gen.restoreGraphicsState(); + } + + public static void writeImage(ImageEncoder encoder, Dimension imgDim, String imgDescription, + Rectangle2D targetRect, ColorModel colorModel, PSGenerator gen) throws IOException { + writeImage(encoder, imgDim, imgDescription, targetRect, colorModel, gen, null); + } + + /** + * Writes a bitmap image to the PostScript stream. + * @param encoder the image encoder + * @param imgDim the dimensions of the image + * @param imgDescription the name of the image + * @param targetRect the target rectangle to place the image in + * @param colorModel the color model of the image + * @param gen the PostScript generator + * @throws IOException In case of an I/O exception + */ + public static void writeImage(ImageEncoder encoder, Dimension imgDim, String imgDescription, + Rectangle2D targetRect, ColorModel colorModel, PSGenerator gen, RenderedImage ri) + throws IOException { + + gen.saveGraphicsState(); + translateAndScale(gen, null, targetRect); + gen.commentln("%AXGBeginBitmap: " + imgDescription); + gen.writeln("{{"); + + String implicitFilter = encoder.getImplicitFilter(); + if (implicitFilter != null) { + gen.writeln("/RawData currentfile /ASCII85Decode filter def"); + gen.writeln("/Data RawData " + implicitFilter + " filter def"); + } else { + if (gen.getPSLevel() >= 3) { + gen.writeln("/RawData currentfile /ASCII85Decode filter def"); + gen.writeln("/Data RawData /FlateDecode filter def"); + } else { + gen.writeln("/RawData currentfile /ASCII85Decode filter def"); + gen.writeln("/Data RawData /RunLengthDecode filter def"); + } + } + + PSDictionary imageDict = new PSDictionary(); + imageDict.put("/DataSource", "Data"); + + populateImageDictionary(imgDim, colorModel, imageDict); + + if (ri != null) { + DataBuffer buffer = ri.getData().getDataBuffer(); + if (!(buffer instanceof DataBufferByte)) { + imageDict.put("/BitsPerComponent", 8); + } + } + writeImageCommand(imageDict, colorModel, gen); + + /* + * the following two lines could be enabled if something still goes wrong + * gen.write("Data closefile"); + * gen.write("RawData flushfile"); + */ + gen.writeln("} stopped {handleerror} if"); + gen.writeln(" RawData flushfile"); + gen.writeln("} exec"); + + compressAndWriteBitmap(encoder, gen); + + gen.newLine(); + gen.commentln("%AXGEndBitmap"); + gen.restoreGraphicsState(); + } + + /** + * Writes a bitmap image to the PostScript stream. + * @param encoder the image encoder + * @param imgDim the dimensions of the image + * @param imgDescription the name of the image + * @param targetRect the target rectangle to place the image in + * @param colorModel the color model of the image + * @param gen the PostScript generator + * @throws IOException In case of an I/O exception + */ + public static void writeImage(ImageEncoder encoder, Dimension imgDim, String imgDescription, + Rectangle2D targetRect, ColorModel colorModel, PSGenerator gen, RenderedImage ri, + Color maskColor) + throws IOException { + + gen.saveGraphicsState(); + translateAndScale(gen, null, targetRect); + gen.commentln("%AXGBeginBitmap: " + imgDescription); + gen.writeln("{{"); + + String implicitFilter = encoder.getImplicitFilter(); + if (implicitFilter != null) { + gen.writeln("/RawData currentfile /ASCII85Decode filter def"); + gen.writeln("/Data RawData " + implicitFilter + " filter def"); + } else { + if (gen.getPSLevel() >= 3) { + gen.writeln("/RawData currentfile /ASCII85Decode filter def"); + gen.writeln("/Data RawData /FlateDecode filter def"); + } else { + gen.writeln("/RawData currentfile /ASCII85Decode filter def"); + gen.writeln("/Data RawData /RunLengthDecode filter def"); + } + } + + PSDictionary imageDict = new PSDictionary(); + imageDict.put("/DataSource", "Data"); + + populateImageDictionary(imgDim, colorModel, imageDict, maskColor); + + if (ri != null) { + DataBuffer buffer = ri.getData().getDataBuffer(); + if (!(buffer instanceof DataBufferByte)) { + imageDict.put("/BitsPerComponent", 8); + } + } + writeImageCommand(imageDict, colorModel, gen); + + /* + * the following two lines could be enabled if something still goes wrong + * gen.write("Data closefile"); + * gen.write("RawData flushfile"); + */ + gen.writeln("} stopped {handleerror} if"); + gen.writeln(" RawData flushfile"); + gen.writeln("} exec"); + + compressAndWriteBitmap(encoder, gen); + + gen.newLine(); + gen.commentln("%AXGEndBitmap"); + gen.restoreGraphicsState(); + } + + private static ColorModel populateImageDictionary(Dimension imgDim, ColorModel colorModel, + PSDictionary imageDict) { + imageDict.put("/ImageType", "1"); + colorModel = writeImageDictionary(imgDim, imageDict, colorModel); + return colorModel; + } + + private static ColorModel populateImageDictionary(Dimension imgDim, ColorModel colorModel, + PSDictionary imageDict, Color maskColor) { + imageDict.put("/ImageType", "4"); + + colorModel = writeImageDictionary(imgDim, imageDict, colorModel); + imageDict.put("/MaskColor", String.format("[ %d %d %d ]", maskColor.getRed(), + maskColor.getGreen(), maskColor.getBlue())); + return colorModel; + } + + private static ColorModel writeImageDictionary(Dimension imgDim, PSDictionary imageDict, + ColorModel colorModel) { + String w = Integer.toString(imgDim.width); + String h = Integer.toString(imgDim.height); + imageDict.put("/Width", w); + imageDict.put("/Height", h); + + boolean invertColors = false; + String decodeArray = getDecodeArray(colorModel.getNumColorComponents(), invertColors); + int bitsPerComp = colorModel.getComponentSize(0); + + // Setup scanning for left-to-right and top-to-bottom + imageDict.put("/ImageMatrix", "[" + w + " 0 0 " + h + " 0 0]"); + + if ((colorModel instanceof IndexColorModel)) { + IndexColorModel indexColorModel = (IndexColorModel) colorModel; + int c = indexColorModel.getMapSize(); + int hival = c - 1; + if (hival > 4095) { + throw new UnsupportedOperationException("hival must not go beyond 4095"); + } + bitsPerComp = indexColorModel.getPixelSize(); + int ceiling = ((int) Math.pow(2, bitsPerComp)) - 1; + decodeArray = "[0 " + ceiling + "]"; + } + imageDict.put("/BitsPerComponent", Integer.toString(bitsPerComp)); + imageDict.put("/Decode", decodeArray); + return colorModel; + } + + private static String getDecodeArray(int numComponents, boolean invertColors) { + String decodeArray; + StringBuffer sb = new StringBuffer("["); + for (int i = 0; i < numComponents; i++) { + if (i > 0) { + sb.append(" "); + } + if (invertColors) { + sb.append("1 0"); + } else { + sb.append("0 1"); + } + } + sb.append("]"); + decodeArray = sb.toString(); + return decodeArray; + } + + private static void prepareColorspace(PSGenerator gen, ColorSpace colorSpace) + throws IOException { + gen.writeln(getColorSpaceName(colorSpace) + " setcolorspace"); + } + + private static void prepareColorSpace(PSGenerator gen, ColorModel cm) throws IOException { + //Prepare color space + if ((cm instanceof IndexColorModel)) { + ColorSpace cs = cm.getColorSpace(); + IndexColorModel im = (IndexColorModel)cm; + boolean isDeviceGray; + int c = im.getMapSize(); + int[] palette = new int[c]; + im.getRGBs(palette); + byte[] reds = new byte[c]; + byte[] greens = new byte[c]; + byte[] blues = new byte[c]; + im.getReds(reds); + im.getGreens(greens); + im.getBlues(blues); + int hival = c - 1; + if (hival > 4095) { + throw new UnsupportedOperationException("hival must not go beyond 4095"); + } + isDeviceGray = Arrays.equals(reds, blues) && Arrays.equals(blues, greens); + if (isDeviceGray) { + gen.write("[/Indexed " + "/DeviceGray"); + } else { + gen.write("[/Indexed " + getColorSpaceName(cs)); + } + gen.writeln(" " + Integer.toString(hival)); + gen.write(" <"); + if (isDeviceGray) { + gen.write(toHexString(blues)); + } else { + for (int i = 0; i < c; i++) { + if (i > 0) { + if ((i % 8) == 0) { + gen.newLine(); + gen.write(" "); + } else { + gen.write(" "); + } + } + gen.write(rgb2Hex(palette[i])); + } + } + gen.writeln(">"); + gen.writeln("] setcolorspace"); + } else { + gen.writeln(getColorSpaceName(cm.getColorSpace()) + " setcolorspace"); + } + } + + static String toHexString(byte[] color) { + char[] hexChars = new char[color.length * 2]; + int x; + for (int i = 0; i < color.length; i++) { + x = color[i] & 0xFF; + hexChars[i * 2] = HEX[x >>> 4]; + hexChars[i * 2 + 1] = HEX[x & 0x0F]; + } + return new String(hexChars); + } + + static void writeImageCommand(RenderedImage img, + PSDictionary imageDict, PSGenerator gen) throws IOException { + ImageEncodingHelper helper = new ImageEncodingHelper(img, true); + ColorModel cm = helper.getEncodedColorModel(); + Dimension imgDim = new Dimension(img.getWidth(), img.getHeight()); + + populateImageDictionary(imgDim, cm, imageDict); + writeImageCommand(imageDict, cm, gen); + } + + static void writeImageCommand(PSDictionary imageDict, ColorModel cm, PSGenerator gen) + throws IOException { + prepareColorSpace(gen, cm); + gen.write(imageDict.toString()); + gen.writeln(" image"); + } + + static void writeImageCommand(PSDictionary imageDict, + Dimension imgDim, ColorSpace colorSpace, boolean invertImage, + PSGenerator gen) throws IOException { + imageDict.put("/ImageType", "1"); + imageDict.put("/Width", Integer.toString(imgDim.width)); + imageDict.put("/Height", Integer.toString(imgDim.height)); + String decodeArray = getDecodeArray(colorSpace.getNumComponents(), invertImage); + imageDict.put("/Decode", decodeArray); + // Setup scanning for left-to-right and top-to-bottom + imageDict.put("/ImageMatrix", "[" + imgDim.width + " 0 0 " + imgDim.height + " 0 0]"); + + prepareColorspace(gen, colorSpace); + gen.write(imageDict.toString()); + gen.writeln(" image"); + } + + private static final char[] HEX = { + '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' + }; + + private static String rgb2Hex(int rgb) { + StringBuffer sb = new StringBuffer(); + for (int i = 5; i >= 0; i--) { + int shift = i * 4; + int n = (rgb & (15 << shift)) >> shift; + sb.append(HEX[n % 16]); + } + return sb.toString(); + } + + /** + * Renders a bitmap image to PostScript. + * @param img image to render + * @param x x position + * @param y y position + * @param w width + * @param h height + * @param gen PS generator + * @throws IOException In case of an I/O problem while rendering the image + */ + public static void renderBitmapImage(RenderedImage img, + float x, float y, float w, float h, PSGenerator gen, Color mask) + throws IOException { + Rectangle2D targetRect = new Rectangle2D.Double(x, y, w, h); + ImageEncoder encoder = ImageEncodingHelper.createRenderedImageEncoder(img); + Dimension imgDim = new Dimension(img.getWidth(), img.getHeight()); + String imgDescription = img.getClass().getName(); + ImageEncodingHelper helper = new ImageEncodingHelper(img); + ColorModel cm = helper.getEncodedColorModel(); + + if (mask == null) { + writeImage(encoder, imgDim, imgDescription, targetRect, cm, gen, img); + } else { + writeImage(encoder, imgDim, imgDescription, targetRect, cm, gen, img, mask); + } + } + + /** + * Writes a bitmap image as a PostScript form enclosed by DSC resource wrappers to the + * PostScript file. + * @param img the raw bitmap data + * @param imgDim the dimensions of the image + * @param formName the name of the PostScript form to use + * @param imageDescription a description of the image added as a DSC Title comment + * @param isJPEG true if "img" contains a DCT-encoded images, false if "img" contains the + * decoded bitmap + * @param colorSpace the color space of the image + * @param gen the PostScript generator + * @return a PSResource representing the form for resource tracking + * @throws IOException In case of an I/O exception + * @deprecated Please use {@link FormGenerator} + */ + public static PSResource writeReusableImage(final byte[] img, + Dimension imgDim, String formName, String imageDescription, + final boolean isJPEG, ColorSpace colorSpace, + PSGenerator gen) throws IOException { + ImageEncoder encoder = new ImageEncoder() { + public void writeTo(OutputStream out) throws IOException { + out.write(img); + } + public String getImplicitFilter() { + if (isJPEG) { + return "<< >> /DCTDecode"; + } else { + return null; + } + } + }; + return writeReusableImage(encoder, imgDim, formName, + imageDescription, colorSpace, false, gen); + } + + /** + * Writes a bitmap image as a PostScript form enclosed by DSC resource wrappers to the + * PostScript file. + * @param encoder the ImageEncoder that will provide the raw bitmap data + * @param imgDim the dimensions of the image + * @param formName the name of the PostScript form to use + * @param imageDescription a description of the image added as a DSC Title comment + * @param colorSpace the color space of the image + * @param invertImage true if the image shall be inverted + * @param gen the PostScript generator + * @return a PSResource representing the form for resource tracking + * @throws IOException In case of an I/O exception + * @deprecated Please use {@link FormGenerator} + */ + protected static PSResource writeReusableImage(ImageEncoder encoder, + Dimension imgDim, + String formName, String imageDescription, + ColorSpace colorSpace, boolean invertImage, + PSGenerator gen) throws IOException { + if (gen.getPSLevel() < 2) { + throw new UnsupportedOperationException( + "Reusable images requires at least Level 2 PostScript"); + } + String dataName = formName + ":Data"; + gen.writeDSCComment(DSCConstants.BEGIN_RESOURCE, formName); + if (imageDescription != null) { + gen.writeDSCComment(DSCConstants.TITLE, imageDescription); + } + + String additionalFilters; + String implicitFilter = encoder.getImplicitFilter(); + if (implicitFilter != null) { + additionalFilters = "/ASCII85Decode filter " + implicitFilter + " filter"; + } else { + if (gen.getPSLevel() >= 3) { + additionalFilters = "/ASCII85Decode filter /FlateDecode filter"; + } else { + additionalFilters = "/ASCII85Decode filter /RunLengthDecode filter"; + } + } + + gen.writeln("/" + formName); + gen.writeln("<< /FormType 1"); + gen.writeln(" /BBox [0 0 " + imgDim.width + " " + imgDim.height + "]"); + gen.writeln(" /Matrix [1 0 0 1 0 0]"); + gen.writeln(" /PaintProc {"); + gen.writeln(" pop"); + gen.writeln(" gsave"); + if (gen.getPSLevel() == 2) { + gen.writeln(" userdict /i 0 put"); //rewind image data + } else { + gen.writeln(" " + dataName + " 0 setfileposition"); //rewind image data + } + String dataSource; + if (gen.getPSLevel() == 2) { + dataSource = "{ " + dataName + " i get /i i 1 add store } bind"; + } else { + dataSource = dataName; + } + PSDictionary imageDict = new PSDictionary(); + imageDict.put("/DataSource", dataSource); + imageDict.put("/BitsPerComponent", Integer.toString(8)); + writeImageCommand(imageDict, imgDim, colorSpace, invertImage, gen); + gen.writeln(" grestore"); + gen.writeln(" } bind"); + gen.writeln(">> def"); + gen.writeln("/" + dataName + " currentfile"); + gen.writeln(additionalFilters); + if (gen.getPSLevel() == 2) { + //Creates a data array from the inline file + gen.writeln("{ /temp exch def [" + + " { temp 16384 string readstring not {exit } if } loop ] } exec"); + } else { + gen.writeln("/ReusableStreamDecode filter"); + } + compressAndWriteBitmap(encoder, gen); + gen.writeln("def"); + gen.writeDSCComment(DSCConstants.END_RESOURCE); + PSResource res = new PSResource(PSResource.TYPE_FORM, formName); + gen.getResourceTracker().registerSuppliedResource(res); + return res; + } + + /** + * Paints a reusable image (previously added as a PostScript form). + * @param formName the name of the PostScript form implementing the image + * @param targetRect the target rectangle to place the image in + * @param gen the PostScript generator + * @throws IOException In case of an I/O exception + * @deprecated Please use {@link #paintForm(PSResource, Dimension2D, Rectangle2D, PSGenerator)} + * instead. + */ + public static void paintReusableImage( + String formName, + Rectangle2D targetRect, + PSGenerator gen) throws IOException { + PSResource form = new PSResource(PSResource.TYPE_FORM, formName); + paintForm(form, null, targetRect, gen); + } + + /** + * Paints a reusable image (previously added as a PostScript form). + * @param form the PostScript form resource implementing the image + * @param targetRect the target rectangle to place the image in + * @param gen the PostScript generator + * @throws IOException In case of an I/O exception + * @deprecated Please use {@link #paintForm(PSResource, Dimension2D, Rectangle2D, PSGenerator)} + * instead. + */ + public static void paintForm( + PSResource form, + Rectangle2D targetRect, + PSGenerator gen) throws IOException { + paintForm(form, null, targetRect, gen); + } + + /** + * Paints a reusable image (previously added as a PostScript form). + * @param form the PostScript form resource implementing the image + * @param formDimensions the original dimensions of the form + * @param targetRect the target rectangle to place the image in + * @param gen the PostScript generator + * @throws IOException In case of an I/O exception + */ + public static void paintForm( + PSResource form, + Dimension2D formDimensions, + Rectangle2D targetRect, + PSGenerator gen) throws IOException { + gen.saveGraphicsState(); + translateAndScale(gen, formDimensions, targetRect); + gen.writeln(form.getName() + " execform"); + + gen.getResourceTracker().notifyResourceUsageOnPage(form); + gen.restoreGraphicsState(); + } + + private static String getColorSpaceName(ColorSpace colorSpace) { + if (colorSpace.getType() == ColorSpace.TYPE_CMYK) { + return "/DeviceCMYK"; + } else if (colorSpace.getType() == ColorSpace.TYPE_GRAY) { + return "/DeviceGray"; + } else { + return "/DeviceRGB"; + } + } + + static void compressAndWriteBitmap(ImageEncoder encoder, PSGenerator gen) + throws IOException { + OutputStream out = gen.getOutputStream(); + out = new ASCII85OutputStream(out); + String implicitFilter = encoder.getImplicitFilter(); + if (implicitFilter != null) { + //nop + } else { + if (gen.getPSLevel() >= 3) { + out = new FlateEncodeOutputStream(out); + } else { + out = new RunLengthEncodeOutputStream(out); + } + } + encoder.writeTo(out); + if (out instanceof Finalizable) { + ((Finalizable)out).finalizeStream(); + } else { + out.flush(); + } + gen.newLine(); //Just to be sure + } + + /** + * Generates commands to modify the current transformation matrix so an image fits + * into a given rectangle. + * @param gen the PostScript generator + * @param imageDimensions the image's dimensions + * @param targetRect the target rectangle + * @throws IOException if an I/O error occurs + */ + public static void translateAndScale(PSGenerator gen, + Dimension2D imageDimensions, Rectangle2D targetRect) + throws IOException { + gen.writeln(gen.formatDouble(targetRect.getX()) + " " + + gen.formatDouble(targetRect.getY()) + " translate"); + if (imageDimensions == null) { + imageDimensions = new Dimension(1, 1); + } + double sx = targetRect.getWidth() / imageDimensions.getWidth(); + double sy = targetRect.getHeight() / imageDimensions.getHeight(); + if (sx != 1 || sy != 1) { + gen.writeln(gen.formatDouble(sx) + " " + + gen.formatDouble(sy) + " scale"); + } + } + + /** + * Extracts a packed RGB integer array of a RenderedImage. + * @param img the image + * @param startX the starting X coordinate + * @param startY the starting Y coordinate + * @param w the width of the cropped image + * @param h the height of the cropped image + * @param rgbArray the prepared integer array to write to + * @param offset offset in the target array + * @param scansize width of a row in the target array + * @return the populated integer array previously passed in as rgbArray parameter + */ + public static int[] getRGB(RenderedImage img, + int startX, int startY, + int w, int h, + int[] rgbArray, int offset, int scansize) { + Raster raster = img.getData(); + int yoff = offset; + int off; + Object data; + int nbands = raster.getNumBands(); + int dataType = raster.getDataBuffer().getDataType(); + switch (dataType) { + case DataBuffer.TYPE_BYTE: + data = new byte[nbands]; + break; + case DataBuffer.TYPE_USHORT: + data = new short[nbands]; + break; + case DataBuffer.TYPE_INT: + data = new int[nbands]; + break; + case DataBuffer.TYPE_FLOAT: + data = new float[nbands]; + break; + case DataBuffer.TYPE_DOUBLE: + data = new double[nbands]; + break; + default: + throw new IllegalArgumentException("Unknown data buffer type: " + + dataType); + } + + if (rgbArray == null) { + rgbArray = new int[offset + h * scansize]; + } + + ColorModel colorModel = img.getColorModel(); + for (int y = startY; y < startY + h; y++, yoff += scansize) { + off = yoff; + for (int x = startX; x < startX + w; x++) { + rgbArray[off++] = colorModel.getRGB(raster.getDataElements(x, y, data)); + } + } + + return rgbArray; + } + + /** + * Places an EPS file in the PostScript stream. + * @param rawEPS byte array containing the raw EPS data + * @param name name for the EPS document + * @param x x-coordinate of viewport in points + * @param y y-coordinate of viewport in points + * @param w width of viewport in points + * @param h height of viewport in points + * @param bboxx x-coordinate of EPS bounding box in points + * @param bboxy y-coordinate of EPS bounding box in points + * @param bboxw width of EPS bounding box in points + * @param bboxh height of EPS bounding box in points + * @param gen the PS generator + * @throws IOException in case an I/O error happens during output + * @deprecated Please use the variant with the InputStream as parameter + */ + public static void renderEPS(byte[] rawEPS, String name, + float x, float y, float w, float h, + float bboxx, float bboxy, float bboxw, float bboxh, + PSGenerator gen) throws IOException { + renderEPS(new java.io.ByteArrayInputStream(rawEPS), name, + new Rectangle2D.Float(x, y, w, h), + new Rectangle2D.Float(bboxx, bboxy, bboxw, bboxh), + gen); + } + + /** + * Places an EPS file in the PostScript stream. + * @param in the InputStream that contains the EPS stream + * @param name name for the EPS document + * @param viewport the viewport in points in which to place the EPS + * @param bbox the EPS bounding box in points + * @param gen the PS generator + * @throws IOException in case an I/O error happens during output + */ + public static void renderEPS(InputStream in, String name, + Rectangle2D viewport, Rectangle2D bbox, + PSGenerator gen) throws IOException { + gen.getResourceTracker().notifyResourceUsageOnPage(PSProcSets.EPS_PROCSET); + gen.writeln("%AXGBeginEPS: " + name); + gen.writeln("BeginEPSF"); + + gen.writeln(gen.formatDouble(viewport.getX()) + + " " + gen.formatDouble(viewport.getY()) + " translate"); + gen.writeln("0 " + gen.formatDouble(viewport.getHeight()) + " translate"); + gen.writeln("1 -1 scale"); + double sx = viewport.getWidth() / bbox.getWidth(); + double sy = viewport.getHeight() / bbox.getHeight(); + if (sx != 1 || sy != 1) { + gen.writeln(gen.formatDouble(sx) + " " + gen.formatDouble(sy) + " scale"); + } + if (bbox.getX() != 0 || bbox.getY() != 0) { + gen.writeln(gen.formatDouble(-bbox.getX()) + + " " + gen.formatDouble(-bbox.getY()) + " translate"); + } + gen.writeln(gen.formatDouble(bbox.getX()) + + " " + gen.formatDouble(bbox.getY()) + + " " + gen.formatDouble(bbox.getWidth()) + + " " + gen.formatDouble(bbox.getHeight()) + " re clip"); + gen.writeln("newpath"); + + PSResource res = new PSResource(PSResource.TYPE_FILE, name); + gen.getResourceTracker().registerSuppliedResource(res); + gen.getResourceTracker().notifyResourceUsageOnPage(res); + gen.writeDSCComment(DSCConstants.BEGIN_DOCUMENT, res.getName()); + IOUtils.copy(in, gen.getOutputStream()); + gen.newLine(); + gen.writeDSCComment(DSCConstants.END_DOCUMENT); + gen.writeln("EndEPSF"); + gen.writeln("%AXGEndEPS"); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/PSPageDeviceDictionary.java b/src/main/java/org/apache/xmlgraphics/ps/PSPageDeviceDictionary.java new file mode 100644 index 0000000..cbe3f76 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/PSPageDeviceDictionary.java @@ -0,0 +1,119 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PSPageDeviceDictionary.java 1715211 2015-11-19 16:35:44Z ssteiner $ */ + +package org.apache.xmlgraphics.ps; + +import java.util.Map; + +/** + * Postscript page device dictionary object + * + * This object is used by the postscript renderer to hold postscript page device + * values. It can also be used to minimize the number of setpagedevice calls when + * DSC compliance is false. + */ +public class PSPageDeviceDictionary extends PSDictionary { + + private static final long serialVersionUID = 845943256485806509L; + + /** + * Whether or not the contents of the dictionary are flushed on retrieval + */ + private boolean flushOnRetrieval; + + /** + * Dictionary content that has not been output/written yet + */ + private PSDictionary unRetrievedContentDictionary; + + /** + * @param key key with which the specified value is to be associated. + * @param value value to be associated with the specified key. + * @return the previous value associated with the key or null + * @see java.util.Map#put(Object, Object) + */ + public Object put(Object key, Object value) { + Object previousValue = super.put(key, value); + if (flushOnRetrieval) { + if (previousValue == null || !previousValue.equals(value)) { + unRetrievedContentDictionary.put(key, value); + } + } + return previousValue; + } + + public void putAll(Map m) { + for (Object x : m.entrySet()) { + Map.Entry e = (Map.Entry) x; + put(e.getKey(), e.getValue()); + } + } + + /** + * @see java.util.Map#clear() + */ + public void clear() { + super.clear(); + if (unRetrievedContentDictionary != null) { + unRetrievedContentDictionary.clear(); + } + } + + /** + * Returns true if this map contains no key-value mappings. + * + * @return true if this map contains no key-value mappings. + */ + public boolean isEmpty() { + if (flushOnRetrieval) { + return unRetrievedContentDictionary.isEmpty(); + } + return super.isEmpty(); + } + + /** + * The contents of the dictionary are flushed when written + * @param flushOnRetrieval boolean value + */ + public void setFlushOnRetrieval(boolean flushOnRetrieval) { + this.flushOnRetrieval = flushOnRetrieval; + if (flushOnRetrieval) { + unRetrievedContentDictionary = new PSDictionary(); + } + } + + /** + * Returns a dictionary string with containing all unwritten content note: + * unnecessary writes are important as there is a device specific + * initgraphics call by the underlying postscript interpreter on every + * setpagedevice call which can result in blank pages etc. + * + * @return unwritten content dictionary string + */ + public String getContent() { + String content; + if (flushOnRetrieval) { + content = unRetrievedContentDictionary.toString(); + unRetrievedContentDictionary.clear(); + } else { + content = super.toString(); + } + return content; + } +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/PSProcSet.java b/src/main/java/org/apache/xmlgraphics/ps/PSProcSet.java new file mode 100644 index 0000000..6b254de --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/PSProcSet.java @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PSProcSet.java 1598621 2014-05-30 14:55:00Z ssteiner $ */ + +package org.apache.xmlgraphics.ps; + +/** + * PSResource subclass that represents a ProcSet resource. + */ +public class PSProcSet extends PSResource { + + private float version; + private int revision; + + /** + * Creates a new instance. + * @param name name of the resource + */ + public PSProcSet(String name) { + this(name, 1.0f, 0); + } + + /** + * Creates a new instance. + * @param name name of the resource + * @param version version of the resource + * @param revision revision of the resource + */ + public PSProcSet(String name, float version, int revision) { + super(TYPE_PROCSET, name); + this.version = version; + this.revision = revision; + } + + /** @return the version */ + public float getVersion() { + return version; + } + + /** @return the revision */ + public int getRevision() { + return revision; + } + + /** @return the <resource> specification as defined in DSC v3.0 spec. */ + public String getResourceSpecification() { + StringBuffer sb = new StringBuffer(); + sb.append(getType()).append(" ").append(PSGenerator.convertStringToDSC(getName())); + sb.append(" ").append(PSGenerator.convertRealToDSC(getVersion())); + sb.append(" ").append(Integer.toString(getRevision())); + return sb.toString(); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/PSProcSets.java b/src/main/java/org/apache/xmlgraphics/ps/PSProcSets.java new file mode 100644 index 0000000..89f3521 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/PSProcSets.java @@ -0,0 +1,361 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PSProcSets.java 1804124 2017-08-04 14:13:54Z ssteiner $ */ + +package org.apache.xmlgraphics.ps; + +import java.io.IOException; +import java.util.Collections; +import java.util.Map; + +/** + * This class defines the basic resources (procsets) used by the Apache XML Graphics project. + * + * @version $Id: PSProcSets.java 1804124 2017-08-04 14:13:54Z ssteiner $ + */ +public final class PSProcSets { + + private PSProcSets() { + } + + /** the standard procset for the XML Graphics project */ + public static final PSResource STD_PROCSET; + /** the EPS procset for the XML Graphics project */ + public static final PSResource EPS_PROCSET = new EPSProcSet(); + + /** the standard command map matching the {@link #STD_PROCSET}. */ + public static final PSCommandMap STD_COMMAND_MAP; + + static { + StdProcSet stdProcSet = new StdProcSet(); + STD_PROCSET = stdProcSet; + STD_COMMAND_MAP = stdProcSet; + } + + /** + * The standard procset used by XML Graphics Commons. + */ + private static class StdProcSet extends PSProcSet implements PSCommandMap { + + /** A Map of standard shorthand macros defined in the {@link StdProcSet}. */ + private static final Map STANDARD_MACROS; + + static { + Map macros = new java.util.HashMap(); + macros.put("moveto", "M"); + macros.put("rmoveto", "RM"); + macros.put("curveto", "C"); + macros.put("lineto", "L"); + macros.put("show", "t"); + macros.put("ashow", "A"); + macros.put("closepath", "cp"); + macros.put("setrgbcolor", "RC"); + macros.put("setgray", "GC"); + macros.put("setcmykcolor", "CC"); + macros.put("newpath", "N"); + macros.put("setmiterlimit", "ML"); + macros.put("setlinewidth", "LC"); + macros.put("setlinewidth", "LW"); + macros.put("setlinejoin", "LJ"); + macros.put("grestore", "GR"); + macros.put("gsave", "GS"); + macros.put("fill", "f"); + macros.put("stroke", "S"); + macros.put("concat", "CT"); + STANDARD_MACROS = Collections.unmodifiableMap(macros); + } + + public StdProcSet() { + super("Apache XML Graphics Std ProcSet", 1.2f, 0); + } + + public void writeTo(PSGenerator gen) throws IOException { + gen.writeDSCComment(DSCConstants.BEGIN_RESOURCE, + new Object[] {TYPE_PROCSET, getName(), + Float.toString(getVersion()), Integer.toString(getRevision())}); + gen.writeDSCComment(DSCConstants.VERSION, + new Object[] {Float.toString(getVersion()), Integer.toString(getRevision())}); + gen.writeDSCComment(DSCConstants.COPYRIGHT, "Copyright 2001-2003,2010 " + + "The Apache Software Foundation. " + + "License terms: http://www.apache.org/licenses/LICENSE-2.0"); + gen.writeDSCComment(DSCConstants.TITLE, + "Basic set of procedures used by the XML Graphics project (Batik and FOP)"); + + gen.writeln("/bd{bind def}bind def"); + gen.writeln("/ld{load def}bd"); + for (Object o : STANDARD_MACROS.entrySet()) { + Map.Entry entry = (Map.Entry) o; + gen.writeln("/" + entry.getValue() + "/" + entry.getKey() + " ld"); + } + + gen.writeln("/re {4 2 roll M"); //define rectangle + gen.writeln("1 index 0 rlineto"); + gen.writeln("0 exch rlineto"); + gen.writeln("neg 0 rlineto"); + gen.writeln("cp } bd"); + + gen.writeln("/_ctm matrix def"); //Holds the current matrix + gen.writeln("/_tm matrix def"); + //BT: save currentmatrix, set _tm to identitymatrix and move to 0/0 + gen.writeln("/BT { _ctm currentmatrix pop matrix _tm copy pop 0 0 moveto } bd"); + //ET: restore last currentmatrix + gen.writeln("/ET { _ctm setmatrix } bd"); + gen.writeln("/iTm { _ctm setmatrix _tm concat } bd"); + gen.writeln("/Tm { _tm astore pop iTm 0 0 moveto } bd"); + + gen.writeln("/ux 0.0 def"); + gen.writeln("/uy 0.0 def"); + + // F + gen.writeln("/F {"); + gen.writeln(" /Tp exch def"); + // gen.writeln(" currentdict exch get"); + gen.writeln(" /Tf exch def"); + gen.writeln(" Tf findfont Tp scalefont setfont"); + gen.writeln(" /cf Tf def /cs Tp def"); + gen.writeln("} bd"); + + gen.writeln("/ULS {currentpoint /uy exch def /ux exch def} bd"); + gen.writeln("/ULE {"); + gen.writeln(" /Tcx currentpoint pop def"); + gen.writeln(" gsave"); + gen.writeln(" newpath"); + gen.writeln(" cf findfont cs scalefont dup"); + gen.writeln(" /FontMatrix get 0 get /Ts exch def /FontInfo get dup"); + gen.writeln(" /UnderlinePosition get Ts mul /To exch def"); + gen.writeln(" /UnderlineThickness get Ts mul /Tt exch def"); + gen.writeln(" ux uy To add moveto Tcx uy To add lineto"); + gen.writeln(" Tt setlinewidth stroke"); + gen.writeln(" grestore"); + gen.writeln("} bd"); + + gen.writeln("/OLE {"); + gen.writeln(" /Tcx currentpoint pop def"); + gen.writeln(" gsave"); + gen.writeln(" newpath"); + gen.writeln(" cf findfont cs scalefont dup"); + gen.writeln(" /FontMatrix get 0 get /Ts exch def /FontInfo get dup"); + gen.writeln(" /UnderlinePosition get Ts mul /To exch def"); + gen.writeln(" /UnderlineThickness get Ts mul /Tt exch def"); + gen.writeln(" ux uy To add cs add moveto Tcx uy To add cs add lineto"); + gen.writeln(" Tt setlinewidth stroke"); + gen.writeln(" grestore"); + gen.writeln("} bd"); + + gen.writeln("/SOE {"); + gen.writeln(" /Tcx currentpoint pop def"); + gen.writeln(" gsave"); + gen.writeln(" newpath"); + gen.writeln(" cf findfont cs scalefont dup"); + gen.writeln(" /FontMatrix get 0 get /Ts exch def /FontInfo get dup"); + gen.writeln(" /UnderlinePosition get Ts mul /To exch def"); + gen.writeln(" /UnderlineThickness get Ts mul /Tt exch def"); + gen.writeln(" ux uy To add cs 10 mul 26 idiv add moveto " + + "Tcx uy To add cs 10 mul 26 idiv add lineto"); + gen.writeln(" Tt setlinewidth stroke"); + gen.writeln(" grestore"); + gen.writeln("} bd"); + + gen.writeln("/QT {"); + gen.writeln("/Y22 exch store"); + gen.writeln("/X22 exch store"); + gen.writeln("/Y21 exch store"); + gen.writeln("/X21 exch store"); + gen.writeln("currentpoint"); + gen.writeln("/Y21 load 2 mul add 3 div exch"); + gen.writeln("/X21 load 2 mul add 3 div exch"); + gen.writeln("/X21 load 2 mul /X22 load add 3 div"); + gen.writeln("/Y21 load 2 mul /Y22 load add 3 div"); + gen.writeln("/X22 load /Y22 load curveto"); + gen.writeln("} bd"); + + gen.writeln("/SSPD {"); + gen.writeln("dup length /d exch dict def"); + gen.writeln("{"); + gen.writeln("/v exch def"); + gen.writeln("/k exch def"); + gen.writeln("currentpagedevice k known {"); + gen.writeln("/cpdv currentpagedevice k get def"); + gen.writeln("v cpdv ne {"); + gen.writeln("/upd false def"); + gen.writeln("/nullv v type /nulltype eq def"); + gen.writeln("/nullcpdv cpdv type /nulltype eq def"); + gen.writeln("nullv nullcpdv or"); + gen.writeln("{"); + gen.writeln("/upd true def"); + gen.writeln("} {"); + gen.writeln("/sametype v type cpdv type eq def"); + gen.writeln("sametype {"); + gen.writeln("v type /arraytype eq {"); + gen.writeln("/vlen v length def"); + gen.writeln("/cpdvlen cpdv length def"); + gen.writeln("vlen cpdvlen eq {"); + gen.writeln("0 1 vlen 1 sub {"); + gen.writeln("/i exch def"); + gen.writeln("/obj v i get def"); + gen.writeln("/cpdobj cpdv i get def"); + gen.writeln("obj cpdobj ne {"); + gen.writeln("/upd true def"); + gen.writeln("exit"); + gen.writeln("} if"); + gen.writeln("} for"); + gen.writeln("} {"); + gen.writeln("/upd true def"); + gen.writeln("} ifelse"); + gen.writeln("} {"); + gen.writeln("v type /dicttype eq {"); + gen.writeln("v {"); + gen.writeln("/dv exch def"); + gen.writeln("/dk exch def"); + gen.writeln("/cpddv cpdv dk get def"); + gen.writeln("dv cpddv ne {"); + gen.writeln("/upd true def"); + gen.writeln("exit"); + gen.writeln("} if"); + gen.writeln("} forall"); + gen.writeln("} {"); + gen.writeln("/upd true def"); + gen.writeln("} ifelse"); + gen.writeln("} ifelse"); + gen.writeln("} if"); + gen.writeln("} ifelse"); + gen.writeln("upd true eq {"); + gen.writeln("d k v put"); + gen.writeln("} if"); + gen.writeln("} if"); + gen.writeln("} if"); + gen.writeln("} forall"); + gen.writeln("d length 0 gt {"); + gen.writeln("d setpagedevice"); + gen.writeln("} if"); + gen.writeln("} bd"); + + gen.writeln("/RE { % /NewFontName [NewEncodingArray] /FontName RE -"); + gen.writeln(" findfont dup length dict begin"); + gen.writeln(" {"); + gen.writeln(" 1 index /FID ne"); + gen.writeln(" {def} {pop pop} ifelse"); + gen.writeln(" } forall"); + gen.writeln(" /Encoding exch def"); + gen.writeln(" /FontName 1 index def"); + gen.writeln(" currentdict definefont pop"); + gen.writeln(" end"); + gen.writeln("} bind def"); + + gen.writeDSCComment(DSCConstants.END_RESOURCE); + gen.getResourceTracker().registerSuppliedResource(this); + } + + /** {@inheritDoc} */ + public String mapCommand(String command) { + String mapped = (String)STANDARD_MACROS.get(command); + return (mapped != null ? mapped : command); + } + + } + + private static class EPSProcSet extends PSProcSet { + + public EPSProcSet() { + super("Apache XML Graphics EPS ProcSet", 1.0f, 0); + } + + public void writeTo(PSGenerator gen) throws IOException { + gen.writeDSCComment(DSCConstants.BEGIN_RESOURCE, + new Object[] {TYPE_PROCSET, getName(), + Float.toString(getVersion()), Integer.toString(getRevision())}); + gen.writeDSCComment(DSCConstants.VERSION, + new Object[] {Float.toString(getVersion()), Integer.toString(getRevision())}); + gen.writeDSCComment(DSCConstants.COPYRIGHT, "Copyright 2002-2003 " + + "The Apache Software Foundation. " + + "License terms: http://www.apache.org/licenses/LICENSE-2.0"); + gen.writeDSCComment(DSCConstants.TITLE, + "EPS procedures used by the Apache XML Graphics project (Batik and FOP)"); + + gen.writeln("/BeginEPSF { %def"); + gen.writeln("/b4_Inc_state save def % Save state for cleanup"); + gen.writeln("/dict_count countdictstack def % Count objects on dict stack"); + gen.writeln("/op_count count 1 sub def % Count objects on operand stack"); + gen.writeln("userdict begin % Push userdict on dict stack"); + gen.writeln("/showpage { } def % Redefine showpage, { } = null proc"); + gen.writeln("0 setgray 0 setlinecap % Prepare graphics state"); + gen.writeln("1 setlinewidth 0 setlinejoin"); + gen.writeln("10 setmiterlimit [ ] 0 setdash newpath"); + gen.writeln("/languagelevel where % If level not equal to 1 then"); + gen.writeln("{pop languagelevel % set strokeadjust and"); + gen.writeln("1 ne % overprint to their defaults."); + gen.writeln("{false setstrokeadjust false setoverprint"); + gen.writeln("} if"); + gen.writeln("} if"); + gen.writeln("} bd"); + + gen.writeln("/EndEPSF { %def"); + gen.writeln("count op_count sub {pop} repeat % Clean up stacks"); + gen.writeln("countdictstack dict_count sub {end} repeat"); + gen.writeln("b4_Inc_state restore"); + gen.writeln("} bd"); + + gen.writeDSCComment(DSCConstants.END_RESOURCE); + gen.getResourceTracker().registerSuppliedResource(this); + } + + } + + /** + * Generates a resource defining standard procset with operations used by the XML Graphics + * project. + * @param gen PSGenerator to use for output + * @throws IOException In case of an I/O problem + */ + public static void writeStdProcSet(PSGenerator gen) throws IOException { + ((StdProcSet)STD_PROCSET).writeTo(gen); + } + + /** + * Generates a resource defining standard procset with operations used by the XML Graphics + * project. + * @param gen PSGenerator to use for output + * @throws IOException In case of an I/O problem + * @deprecated Use writeStdProcSet() instead. + */ + public static void writeFOPStdProcSet(PSGenerator gen) throws IOException { + writeStdProcSet(gen); + } + + + /** + * Generates a resource defining a procset for including EPS graphics. + * @param gen PSGenerator to use for output + * @throws IOException In case of an I/O problem + */ + public static void writeEPSProcSet(PSGenerator gen) throws IOException { + ((EPSProcSet)EPS_PROCSET).writeTo(gen); + } + + /** + * Generates a resource defining a procset for including EPS graphics. + * @param gen PSGenerator to use for output + * @throws IOException In case of an I/O problem + * @deprecated Use writeEPSProcSet() instead. + */ + public static void writeFOPEPSProcSet(PSGenerator gen) throws IOException { + writeEPSProcSet(gen); + } + + +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/PSResource.java b/src/main/java/org/apache/xmlgraphics/ps/PSResource.java new file mode 100644 index 0000000..e4f5589 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/PSResource.java @@ -0,0 +1,109 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PSResource.java 1598621 2014-05-30 14:55:00Z ssteiner $ */ + +package org.apache.xmlgraphics.ps; + +/** + * Represents a PostScript resource (file, font, procset etc.). + */ +public class PSResource implements Comparable { + + /** a file resource */ + public static final String TYPE_FILE = "file"; + /** a font resource */ + public static final String TYPE_FONT = "font"; + /** a procset resource */ + public static final String TYPE_PROCSET = "procset"; + /** a procset resource */ + public static final String TYPE_PATTERN = "pattern"; + /** a procset resource */ + public static final String TYPE_FORM = "form"; + /** a procset resource */ + public static final String TYPE_ENCODING = "encoding"; + /** A CMap resource. */ + public static final String TYPE_CMAP = "cmap"; + /** A CIDFont resource. */ + public static final String TYPE_CIDFONT = "cidfont"; + + private String type; + private String name; + + /** + * Main constructor + * @param type type of the resource + * @param name name of the resource + */ + public PSResource(String type, String name) { + this.type = type; + this.name = name; + } + + /** @return the type of the resource */ + public String getType() { + return this.type; + } + + /** @return the name of the resource */ + public String getName() { + return this.name; + } + + /** @return the <resource> specification as defined in DSC v3.0 spec. */ + public String getResourceSpecification() { + StringBuffer sb = new StringBuffer(); + sb.append(getType()).append(" ").append(PSGenerator.convertStringToDSC(getName())); + return sb.toString(); + } + + /** {@inheritDoc} */ + public boolean equals(Object obj) { + if (obj == this) { + return true; + } else if (obj instanceof PSResource) { + PSResource other = (PSResource)obj; + return other.toString().equals(toString()); + } else { + return false; + } + } + + /** {@inheritDoc} */ + public int hashCode() { + return toString().hashCode(); + } + + /** {@inheritDoc} */ + public int compareTo(Object o) { + PSResource other = (PSResource)o; + if (this == other) { + return 0; + } + int result = this.getType().compareTo(other.getType()); + if (result == 0) { + result = this.getName().compareTo(other.getName()); + } + return result; + } + + /** {@inheritDoc} */ + public String toString() { + return getResourceSpecification(); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/PSState.java b/src/main/java/org/apache/xmlgraphics/ps/PSState.java new file mode 100644 index 0000000..6d52bb1 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/PSState.java @@ -0,0 +1,236 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PSState.java 1804124 2017-08-04 14:13:54Z ssteiner $ */ + +package org.apache.xmlgraphics.ps; + +import java.awt.Color; +import java.awt.geom.AffineTransform; +import java.io.IOException; +import java.io.Serializable; +import java.util.List; + +import org.apache.xmlgraphics.java2d.color.ColorUtil; + +/** + * This class holds the current state of the PostScript interpreter. + * + * @version $Id: PSState.java 1804124 2017-08-04 14:13:54Z ssteiner $ + */ +public class PSState implements Serializable { + + /** Default for setdash */ + public static final String DEFAULT_DASH = "[] 0"; + /** Default color in PostScript */ + public static final Color DEFAULT_RGB_COLOR = Color.black; + private static final long serialVersionUID = -3862731539801753248L; + + private AffineTransform transform = new AffineTransform(); + private List transformConcatList = new java.util.ArrayList(); + + private int linecap; + private int linejoin; + private float miterLimit; + private double linewidth = 1.0f; + private String dashpattern = DEFAULT_DASH; + private Color color = DEFAULT_RGB_COLOR; + + //Font state + private String fontname; + private float fontsize; + + /** + * Default constructor + */ + public PSState() { + //nop + } + + /** + * Copy constructor + * @param org the original to copy from + * @param copyTransforms true if the list of matrix concats should be cloned, too + */ + public PSState(PSState org, boolean copyTransforms) { + this.transform = (AffineTransform)org.transform.clone(); + if (copyTransforms) { + this.transformConcatList.addAll(org.transformConcatList); + } + this.linecap = org.linecap; + this.linejoin = org.linejoin; + this.miterLimit = org.miterLimit; + this.linewidth = org.linewidth; + this.dashpattern = org.dashpattern; + this.color = org.color; + this.fontname = org.fontname; + this.fontsize = org.fontsize; + } + + /** + * Returns the transform. + * @return the current transformation matrix + */ + public AffineTransform getTransform() { + return this.transform; + } + + /** + * Check the current transform. + * The transform for the current state is the combination of all + * transforms in the current state. The parameter is compared + * against this current transform. + * + * @param tf the transform the check against + * @return true if the new transform is different then the current transform + */ + public boolean checkTransform(AffineTransform tf) { + return !tf.equals(this.transform); + } + + /** + * Concats the given transformation matrix with the current one. + * @param transform The new transformation matrix + */ + public void concatMatrix(AffineTransform transform) { + this.transformConcatList.add(transform); + this.transform.concatenate(transform); + } + + /** + * Establishes the specified line cap. + * @param value line cap (0, 1 or 2) as defined by the setlinecap command + * @return true if the line cap changed compared to the previous setting + */ + public boolean useLineCap(int value) { + if (linecap != value) { + linecap = value; + return true; + } else { + return false; + } + } + + /** + * Establishes the specified line join. + * @param value line join (0, 1 or 2) as defined by the setlinejoin command + * @return true if the line join changed compared to the previous setting + */ + public boolean useLineJoin(int value) { + if (linejoin != value) { + linejoin = value; + return true; + } else { + return false; + } + } + + /** + * Establishes the specified miter limit. + * @param value the miter limit as defined by the setmiterlimit command + * @return true if the miter limit changed compared to the previous setting + */ + public boolean useMiterLimit(float value) { + if (miterLimit != value) { + miterLimit = value; + return true; + } else { + return false; + } + } + + /** + * Establishes the specified line width. + * @param value line width as defined by the setlinewidth command + * @return true if the line width changed compared to the previous setting + */ + public boolean useLineWidth(double value) { + if (linewidth != value) { + linewidth = value; + return true; + } else { + return false; + } + } + + /** + * Establishes the specified dash. + * @param pattern dash pattern as defined by the setdash command + * @return true if the dash pattern changed compared to the previous setting + */ + public boolean useDash(String pattern) { + if (!dashpattern.equals(pattern)) { + dashpattern = pattern; + return true; + } else { + return false; + } + } + + /** + * Establishes the specified color (RGB). + * @param value color as defined by the setrgbcolor command + * @return true if the color changed compared to the previous setting + */ + public boolean useColor(Color value) { + if (!ColorUtil.isSameColor(color, value)) { + color = value; + return true; + } else { + return false; + } + } + + /** + * Establishes the specified font and size. + * @param name name of the font for the "F" command (see FOP Std Proc Set) + * @param size size of the font + * @return true if the font changed compared to the previous setting + */ + public boolean useFont(String name, float size) { + if (name == null) { + throw new NullPointerException("font name must not be null"); + } + if (fontname == null || !fontname.equals(name) || fontsize != size) { + fontname = name; + fontsize = size; + return true; + } else { + return false; + } + } + + /** + * Reestablishes the graphics state represented by this instance by issueing the + * necessary commands. + * @param gen The generator to use for output + * @exception IOException In case of an I/O problem + */ + public void reestablish(PSGenerator gen) throws IOException { + for (Object aTransformConcatList : transformConcatList) { + gen.concatMatrix((AffineTransform) aTransformConcatList); + } + gen.useLineCap(linecap); + gen.useLineWidth(linewidth); + gen.useDash(dashpattern); + gen.useColor(color); + if (fontname != null) { + gen.useFont(fontname, fontsize); + } + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/dsc/DSCCommentFactory.java b/src/main/java/org/apache/xmlgraphics/ps/dsc/DSCCommentFactory.java new file mode 100644 index 0000000..e49b9a0 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/dsc/DSCCommentFactory.java @@ -0,0 +1,116 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DSCCommentFactory.java 1780540 2017-01-27 11:10:50Z ssteiner $ */ + +package org.apache.xmlgraphics.ps.dsc; + +import java.lang.reflect.InvocationTargetException; +import java.util.Map; + +import org.apache.xmlgraphics.ps.DSCConstants; +import org.apache.xmlgraphics.ps.dsc.events.DSCComment; +import org.apache.xmlgraphics.ps.dsc.events.DSCCommentBeginDocument; +import org.apache.xmlgraphics.ps.dsc.events.DSCCommentBeginResource; +import org.apache.xmlgraphics.ps.dsc.events.DSCCommentBoundingBox; +import org.apache.xmlgraphics.ps.dsc.events.DSCCommentDocumentNeededResources; +import org.apache.xmlgraphics.ps.dsc.events.DSCCommentDocumentSuppliedResources; +import org.apache.xmlgraphics.ps.dsc.events.DSCCommentEndComments; +import org.apache.xmlgraphics.ps.dsc.events.DSCCommentEndOfFile; +import org.apache.xmlgraphics.ps.dsc.events.DSCCommentHiResBoundingBox; +import org.apache.xmlgraphics.ps.dsc.events.DSCCommentIncludeResource; +import org.apache.xmlgraphics.ps.dsc.events.DSCCommentLanguageLevel; +import org.apache.xmlgraphics.ps.dsc.events.DSCCommentPage; +import org.apache.xmlgraphics.ps.dsc.events.DSCCommentPageBoundingBox; +import org.apache.xmlgraphics.ps.dsc.events.DSCCommentPageHiResBoundingBox; +import org.apache.xmlgraphics.ps.dsc.events.DSCCommentPageResources; +import org.apache.xmlgraphics.ps.dsc.events.DSCCommentPages; +import org.apache.xmlgraphics.ps.dsc.events.DSCCommentTitle; + +/** + * Factory for DSCComment subclasses. + */ +public final class DSCCommentFactory { + + private DSCCommentFactory() { + } + + private static final Map DSC_FACTORIES = new java.util.HashMap(); + + static { + DSC_FACTORIES.put(DSCConstants.END_COMMENTS, + DSCCommentEndComments.class); + DSC_FACTORIES.put(DSCConstants.BEGIN_RESOURCE, + DSCCommentBeginResource.class); + DSC_FACTORIES.put(DSCConstants.INCLUDE_RESOURCE, + DSCCommentIncludeResource.class); + DSC_FACTORIES.put(DSCConstants.PAGE_RESOURCES, + DSCCommentPageResources.class); + DSC_FACTORIES.put(DSCConstants.BEGIN_DOCUMENT, + DSCCommentBeginDocument.class); + DSC_FACTORIES.put(DSCConstants.PAGE, + DSCCommentPage.class); + DSC_FACTORIES.put(DSCConstants.PAGES, + DSCCommentPages.class); + DSC_FACTORIES.put(DSCConstants.BBOX, + DSCCommentBoundingBox.class); + DSC_FACTORIES.put(DSCConstants.HIRES_BBOX, + DSCCommentHiResBoundingBox.class); + DSC_FACTORIES.put(DSCConstants.PAGE_BBOX, + DSCCommentPageBoundingBox.class); + DSC_FACTORIES.put(DSCConstants.PAGE_HIRES_BBOX, + DSCCommentPageHiResBoundingBox.class); + DSC_FACTORIES.put(DSCConstants.LANGUAGE_LEVEL, + DSCCommentLanguageLevel.class); + DSC_FACTORIES.put(DSCConstants.DOCUMENT_NEEDED_RESOURCES, + DSCCommentDocumentNeededResources.class); + DSC_FACTORIES.put(DSCConstants.DOCUMENT_SUPPLIED_RESOURCES, + DSCCommentDocumentSuppliedResources.class); + DSC_FACTORIES.put(DSCConstants.TITLE, + DSCCommentTitle.class); + DSC_FACTORIES.put(DSCConstants.EOF, + DSCCommentEndOfFile.class); + //TODO Add additional implementations as needed + } + + /** + * Creates and returns new instances for DSC comments with a given name. + * @param name the name of the DSCComment (without the "%%" prefix) + * @return the new instance or null if no particular subclass registered for the given + * DSC comment. + */ + public static DSCComment createDSCCommentFor(String name) { + Class clazz = (Class)DSC_FACTORIES.get(name); + if (clazz == null) { + return null; + } + try { + return (DSCComment)clazz.getDeclaredConstructor().newInstance(); + } catch (InstantiationException e) { + throw new RuntimeException("Error instantiating instance for '" + name + "': " + + e.getMessage()); + } catch (IllegalAccessException e) { + throw new RuntimeException("Illegal Access error while instantiating instance for '" + + name + "': " + e.getMessage()); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } catch (InvocationTargetException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/dsc/DSCException.java b/src/main/java/org/apache/xmlgraphics/ps/dsc/DSCException.java new file mode 100644 index 0000000..73c6596 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/dsc/DSCException.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DSCException.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.ps.dsc; + +/** + * Exception to signal problems while parsing a supposedly DSC-conformant file. + */ +public class DSCException extends Exception { + + private static final long serialVersionUID = -1549406547978409615L; + + /** + * Creates a new DSCException. + * @param msg the exception message + */ + public DSCException(String msg) { + super(msg); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/dsc/DSCFilter.java b/src/main/java/org/apache/xmlgraphics/ps/dsc/DSCFilter.java new file mode 100644 index 0000000..085cabe --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/dsc/DSCFilter.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DSCFilter.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.ps.dsc; + +import org.apache.xmlgraphics.ps.dsc.events.DSCEvent; + +/** + * Filter interface for DSC events used by the DSC parser. + */ +public interface DSCFilter { + + /** + * Indicates whether a particular event is acceptable or if it should be skipped/ignored. + * @param event the DSC event + * @return true if the event should be accepted + */ + boolean accept(DSCEvent event); + +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/dsc/DSCHandler.java b/src/main/java/org/apache/xmlgraphics/ps/dsc/DSCHandler.java new file mode 100644 index 0000000..402fc10 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/dsc/DSCHandler.java @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DSCHandler.java 1345683 2012-06-03 14:50:33Z gadams $ */ + +package org.apache.xmlgraphics.ps.dsc; + +import java.io.IOException; + +import org.apache.xmlgraphics.ps.dsc.events.DSCComment; + +/** + * Interface containing events generated by the DSCParser. Applications can implement this + * interface to react to certain events. + */ +public interface DSCHandler { + + /** + * Called as a new PostScript file starts. + * @param header the first line of the DSC-compliant file + * @throws IOException In case of an I/O error + */ + void startDocument(String header) throws IOException; + + /** + * Called when the PostScript file is fully processed, i.e. after the %%EOF comment. + * @throws IOException In case of an I/O error + */ + void endDocument() throws IOException; + + /** + * Called for each standard DSC comment. The classes passed to this method may be simple + * DSCComment classes or special subclasses for some of the DSC comments. + * @param comment the DSC comment + * @throws IOException In case of an I/O error + */ + void handleDSCComment(DSCComment comment) throws IOException; + + /** + * Called for a normal line of PostScript code. + * @param line the line of code + * @throws IOException In case of an I/O error + */ + void line(String line) throws IOException; + + /** + * Called for any line containing a full-line PostScript comment. This is also called for + * custom comments following the extension mechanism of the DSC specification. + * @param comment the comment line + * @throws IOException In case of an I/O error + */ + void comment(String comment) throws IOException; + +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/dsc/DSCListener.java b/src/main/java/org/apache/xmlgraphics/ps/dsc/DSCListener.java new file mode 100644 index 0000000..d2a55f2 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/dsc/DSCListener.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DSCListener.java 727407 2008-12-17 15:05:45Z jeremias $ */ + +package org.apache.xmlgraphics.ps.dsc; + +import java.io.IOException; + +import org.apache.xmlgraphics.ps.dsc.events.DSCEvent; + +/** + * Listener interface for the DSC parser. It can be used to be notified + */ +public interface DSCListener { + + /** + * Called for each DSC event. You can call methods on the DSC parser to skip comments, + * for example. But implementations need to be good citizens and take into account that + * multiple listeners can be active at the same time and that they might interfere with + * other listeners. When returning from the call, state information such as filters should + * be restored. + * @param event the DSC event + * @param parser the DSC parser + * @throws IOException if an I/O error occurs + * @throws DSCException if a DSC-specific error occurs + */ + void processEvent(DSCEvent event, DSCParser parser) throws IOException, DSCException; + +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/dsc/DSCParser.java b/src/main/java/org/apache/xmlgraphics/ps/dsc/DSCParser.java new file mode 100644 index 0000000..419eb70 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/dsc/DSCParser.java @@ -0,0 +1,490 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DSCParser.java 1804124 2017-08-04 14:13:54Z ssteiner $ */ + +package org.apache.xmlgraphics.ps.dsc; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.util.List; +import java.util.NoSuchElementException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.apache.xmlgraphics.ps.DSCConstants; +import org.apache.xmlgraphics.ps.PSGenerator; +import org.apache.xmlgraphics.ps.dsc.events.DSCAtend; +import org.apache.xmlgraphics.ps.dsc.events.DSCComment; +import org.apache.xmlgraphics.ps.dsc.events.DSCEvent; +import org.apache.xmlgraphics.ps.dsc.events.DSCHeaderComment; +import org.apache.xmlgraphics.ps.dsc.events.PostScriptComment; +import org.apache.xmlgraphics.ps.dsc.events.PostScriptLine; +import org.apache.xmlgraphics.ps.dsc.events.UnparsedDSCComment; +import org.apache.xmlgraphics.ps.dsc.tools.DSCTools; + +/** + * Parser for DSC-compliant PostScript files (DSC = Document Structuring Conventions). The parser + * is implemented as a pull parser but has the ability to act as a push parser through the + * DSCHandler interface. + */ +public class DSCParser implements DSCParserConstants { + private static final Log LOG = LogFactory.getLog(DSCParser.class); + + private InputStream in; + private BufferedReader reader; + private boolean eofFound; + private boolean checkEOF = true; + private DSCEvent currentEvent; + private DSCEvent nextEvent; + private DSCListener nestedDocumentHandler; + private DSCListener filterListener; + private List listeners; + private boolean listenersDisabled; + + /** + * Creates a new DSC parser. + * @param in InputStream to read the PostScript file from + * (the stream is not closed by this class, the caller is responsible for that) + * @throws IOException In case of an I/O error + * @throws DSCException In case of a violation of the DSC spec + */ + public DSCParser(InputStream in) throws IOException, DSCException { + if (in.markSupported()) { + this.in = in; + } else { + //Decorate for better performance + this.in = new java.io.BufferedInputStream(this.in); + } + String encoding = "US-ASCII"; + try { + this.reader = new java.io.BufferedReader( + new java.io.InputStreamReader(this.in, encoding)); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("Incompatible VM! " + e.getMessage()); + } + parseNext(); + } + + /** + * Returns the InputStream the PostScript code is read from. + * @return the InputStream the PostScript code is read from + */ + public InputStream getInputStream() { + return this.in; + } + + /** + * This method is used to write out warning messages for the parsing process. Subclass to + * override this method. The default implementation writes to logger. + * @param msg the warning message + */ + protected void warn(String msg) { + LOG.warn(msg); + } + + /** + * Reads one line from the input file + * @return the line or null if there are no more lines + * @throws IOException In case of an I/O error + * @throws DSCException In case of a violation of the DSC spec + */ + protected String readLine() throws IOException, DSCException { + String line; + line = this.reader.readLine(); + checkLine(line); + + return line; + } + + private void checkLine(String line) throws DSCException { + if (line == null) { + if (!eofFound) { + throw new DSCException("%%EOF not found. File is not well-formed."); + } + } else if (line.length() > 255) { + warn("Line longer than 255 characters. This file is not fully PostScript conforming."); + } + } + + private boolean isWhitespace(char c) { + return c == ' ' || c == '\t'; + } + + private DSCComment parseDSCLine(String line) throws IOException, DSCException { + int colon = line.indexOf(':'); + String name; + StringBuilder value = new StringBuilder(); + if (colon > 0) { + name = line.substring(2, colon); + int startOfValue = colon + 1; + if (startOfValue < line.length()) { + if (isWhitespace(line.charAt(startOfValue))) { + startOfValue++; + } + value = new StringBuilder(line.substring(startOfValue).trim()); + if (value.toString().equals(DSCConstants.ATEND.toString())) { + return new DSCAtend(name); + } + } + String nextLine; + while (true) { + this.reader.mark(512); + nextLine = readLine(); + if (nextLine == null) { + break; + } else if (!nextLine.startsWith("%%+")) { + break; + } + value.append(nextLine.substring(3)); + } + this.reader.reset(); + } else { + name = line.substring(2); + return parseDSCComment(name, null); + } + return parseDSCComment(name, value.toString()); + } + + private DSCComment parseDSCComment(String name, String value) { + DSCComment parsed = DSCCommentFactory.createDSCCommentFor(name); + if (parsed != null) { + try { + parsed.parseValue(value); + return parsed; + } catch (Exception e) { + //ignore and fall back to unparsed DSC comment + } + } + UnparsedDSCComment unparsed = new UnparsedDSCComment(name); + unparsed.parseValue(value); + return unparsed; + } + + /** + * Starts the parser in push parsing mode sending events to the DSCHandler instance. + * @param handler the DSCHandler instance to send the events to + * @throws IOException In case of an I/O error + * @throws DSCException In case of a violation of the DSC spec + */ + public void parse(DSCHandler handler) throws IOException, DSCException { + DSCHeaderComment header = DSCTools.checkAndSkipDSC30Header(this); + handler.startDocument("%!" + header.getComment()); + DSCEvent event; + while (hasNext()) { + event = nextEvent(); + switch (event.getEventType()) { + case HEADER_COMMENT: + handler.startDocument("%!" + ((DSCHeaderComment)event).getComment()); + break; + case DSC_COMMENT: + handler.handleDSCComment(event.asDSCComment()); + break; + case COMMENT: + handler.comment(((PostScriptComment)event).getComment()); + break; + case LINE: + handler.line(getLine()); + break; + case EOF: + handler.endDocument(); + break; + default: + throw new IllegalStateException("Illegal event type: " + event.getEventType()); + } + } + } + + /** + * Indicates whether there are additional items. + * @return true if there are additonal items, false if the end of the file has been reached + */ + public boolean hasNext() { + return (this.nextEvent != null); + } + + /** + * Steps to the next item indicating the type of event. + * @return the type of event (See {@link DSCParserConstants}) + * @throws IOException In case of an I/O error + * @throws DSCException In case of a violation of the DSC spec + * @throws NoSuchElementException If an attempt was made to advance beyond the end of the file + */ + public int next() throws IOException, DSCException { + if (hasNext()) { + this.currentEvent = nextEvent; + parseNext(); + + processListeners(); + + return this.currentEvent.getEventType(); + } else { + throw new NoSuchElementException("There are no more events"); + } + } + + private void processListeners() throws IOException, DSCException { + if (isListenersDisabled()) { + return; + } + if (this.filterListener != null) { + //Filter always comes first + this.filterListener.processEvent(this.currentEvent, this); + } + if (this.listeners != null) { + for (Object listener : this.listeners) { + ((DSCListener) listener).processEvent(this.currentEvent, this); + } + } + } + + /** + * Steps to the next item returning the new event. + * @return the new event + * @throws IOException In case of an I/O error + * @throws DSCException In case of a violation of the DSC spec + */ + public DSCEvent nextEvent() throws IOException, DSCException { + next(); + return getCurrentEvent(); + } + + /** + * Returns the current event. + * @return the current event + */ + public DSCEvent getCurrentEvent() { + return this.currentEvent; + } + + /** + * Returns the next event without moving the cursor to the next event. + * @return the next event + */ + public DSCEvent peek() { + return this.nextEvent; + } + + /** + * Parses the next event. + * @throws IOException In case of an I/O error + * @throws DSCException In case of a violation of the DSC spec + */ + protected void parseNext() throws IOException, DSCException { + String line = readLine(); + if (line != null) { + if (isCheckEOF() && eofFound && (line.length() > 0)) { + throw new DSCException("Content found after EOF"); + } + if (line.startsWith("%%")) { + DSCComment comment = parseDSCLine(line); + if (comment.getEventType() == EOF) { + this.eofFound = true; + } + this.nextEvent = comment; + } else if (line.startsWith("%!")) { + this.nextEvent = new DSCHeaderComment(line.substring(2)); + } else if (line.startsWith("%")) { + this.nextEvent = new PostScriptComment(line.substring(1)); + } else { + this.nextEvent = new PostScriptLine(line); + } + } else { + this.nextEvent = null; + } + } + + /** + * Returns the current PostScript line. + * @return the current PostScript line + * @throws IllegalStateException if the current event is not a normal PostScript line + */ + public String getLine() { + if (this.currentEvent.getEventType() == LINE) { + return ((PostScriptLine)this.currentEvent).getLine(); + } else { + throw new IllegalStateException("Current event is not a PostScript line"); + } + } + + /** + * Advances to the next DSC comment with the given name. + * @param name the name of the DSC comment + * @return the requested DSC comment or null if the end of the file is reached + * @throws IOException In case of an I/O error + * @throws DSCException In case of a violation of the DSC spec + */ + public DSCComment nextDSCComment(String name) + throws IOException, DSCException { + return nextDSCComment(name, null); + } + + /** + * Advances to the next DSC comment with the given name. + * @param name the name of the DSC comment + * @param gen PSGenerator to pass the skipped events though to + * @return the requested DSC comment or null if the end of the file is reached + * @throws IOException In case of an I/O error + * @throws DSCException In case of a violation of the DSC spec + */ + public DSCComment nextDSCComment(String name, PSGenerator gen) + throws IOException, DSCException { + while (hasNext()) { + DSCEvent event = nextEvent(); + if (event.isDSCComment()) { + DSCComment comment = event.asDSCComment(); + if (name.equals(comment.getName())) { + return comment; + } + } + if (gen != null) { + event.generate(gen); //Pipe through to PSGenerator + } + } + return null; + } + + /** + * Advances to the next PostScript comment with the given prefix. This is used to find + * comments following the DSC extension mechanism. + *

    + * Example: To find FOP's custom comments, pass in "FOP" as a prefix. This will find comments + * like "%FOPFontSetup". + * @param prefix the prefix of the extension comment + * @param gen PSGenerator to pass the skipped events though to + * @return the requested PostScript comment or null if the end of the file is reached + * @throws IOException In case of an I/O error + * @throws DSCException In case of a violation of the DSC spec + */ + public PostScriptComment nextPSComment(String prefix, PSGenerator gen) + throws IOException, DSCException { + while (hasNext()) { + DSCEvent event = nextEvent(); + if (event.isComment()) { + PostScriptComment comment = (PostScriptComment)event; + if (comment.getComment().startsWith(prefix)) { + return comment; + } + } + if (gen != null) { + event.generate(gen); //Pipe through to PSGenerator + } + } + return null; + } + + /** + * Sets a filter for DSC events. + * @param filter the filter to use or null to disable filtering + */ + public void setFilter(DSCFilter filter) { + if (filter != null) { + this.filterListener = new FilteringEventListener(filter); + } else { + this.filterListener = null; + } + } + + /** + * Adds a DSC event listener. + * @param listener the listener + */ + public void addListener(DSCListener listener) { + if (listener == null) { + throw new NullPointerException("listener must not be null"); + } + if (this.listeners == null) { + this.listeners = new java.util.ArrayList(); + } + this.listeners.add(listener); + } + + /** + * Removes a DSC event listener. + * @param listener the listener to remove + */ + public void removeListener(DSCListener listener) { + if (this.listeners != null) { + this.listeners.remove(listener); + } + } + + /** + * Allows to disable all listeners. This can be used to disable any filtering, for example in + * nested documents. + * @param value true to disable all listeners, false to re-enable them + */ + public void setListenersDisabled(boolean value) { + this.listenersDisabled = value; + } + + /** + * Indicates whether the listeners are currently disabled. + * @return true if they are disabled + */ + public boolean isListenersDisabled() { + return this.listenersDisabled; + } + + /** + * Sets a NestedDocumentHandler which is used to skip nested documents like embedded EPS files. + * You can also process those parts in a special way. + *

    + * It is suggested to use the more generally usable {@link #addListener(DSCListener)} and + * {@link #removeListener(DSCListener)} instead. NestedDocumentHandler is internally + * mapped onto a {@link DSCListener}. + * @param handler the NestedDocumentHandler instance or null to disable the feature + */ + public void setNestedDocumentHandler(final NestedDocumentHandler handler) { + if (handler == null) { + removeListener(this.nestedDocumentHandler); + } else { + MyDSCListener l = new MyDSCListener(); + l.handler = handler; + addListener(l); + } + } + + static class MyDSCListener implements DSCListener { + private NestedDocumentHandler handler; + public void processEvent(DSCEvent event, DSCParser parser) throws IOException, + DSCException { + handler.handle(event, parser); + } + } + + /** + * Tells the parser whether to check for content after the EOF comment. + * This can be disabled to skip nested documents. + * @param value true if the check is enabled + */ + public void setCheckEOF(boolean value) { + this.checkEOF = value; + } + + /** + * Indicates whether the parser is configured to check for content after the EOF comment. + * @return true if the check is enabled. + */ + public boolean isCheckEOF() { + return this.checkEOF; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/dsc/DSCParserConstants.java b/src/main/java/org/apache/xmlgraphics/ps/dsc/DSCParserConstants.java new file mode 100644 index 0000000..15705b5 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/dsc/DSCParserConstants.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DSCParserConstants.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.ps.dsc; + +/** + * Constants the DSC parser uses. + */ +public interface DSCParserConstants { + + /** Indicates a header comment (starting with "%!") */ + int HEADER_COMMENT = 0; + /** Indicates a DSC comment (starting with "%%") */ + int DSC_COMMENT = 1; + /** Indicates a normal PostScript comment (starting with "%") */ + int COMMENT = 2; + /** Indicates a normal PostScript line */ + int LINE = 3; + /** Indicates the end of the file (equivalent to the "%%EOF" DSC comment) */ + int EOF = 4; + +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/dsc/DefaultDSCHandler.java b/src/main/java/org/apache/xmlgraphics/ps/dsc/DefaultDSCHandler.java new file mode 100644 index 0000000..5736372 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/dsc/DefaultDSCHandler.java @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DefaultDSCHandler.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.ps.dsc; + +import java.io.IOException; +import java.io.OutputStream; + +import org.apache.xmlgraphics.ps.DSCConstants; +import org.apache.xmlgraphics.ps.PSGenerator; +import org.apache.xmlgraphics.ps.dsc.events.DSCComment; + +/** + * Default implementation of a DSCHandler which simply passes through the PostScript content + * unchanged. Subclasses can implement different behaviour, for example to filter certain + * DSC comments or to insert PostScript code at specific places. + */ +public class DefaultDSCHandler implements DSCHandler { + + protected OutputStream out; + protected PSGenerator gen; + + /** + * Creates a new instance. + * @param out OutputStream to pipe all received events to + */ + public DefaultDSCHandler(OutputStream out) { + this.out = out; + this.gen = new PSGenerator(this.out); + } + + /** @see org.apache.xmlgraphics.ps.dsc.DSCHandler#startDocument(java.lang.String) */ + public void startDocument(String header) throws IOException { + gen.writeln(header); + } + + /** @see org.apache.xmlgraphics.ps.dsc.DSCHandler#endDocument() */ + public void endDocument() throws IOException { + gen.writeDSCComment(DSCConstants.EOF); + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.DSCHandler#handleDSCComment( + * org.apache.xmlgraphics.ps.dsc.events.DSCComment) + */ + public void handleDSCComment(DSCComment comment) throws IOException { + comment.generate(gen); + + } + + /** @see org.apache.xmlgraphics.ps.dsc.DSCHandler#line(java.lang.String) */ + public void line(String line) throws IOException { + gen.writeln(line); + } + + /** @see org.apache.xmlgraphics.ps.dsc.DSCHandler#comment(java.lang.String) */ + public void comment(String comment) throws IOException { + gen.commentln("%" + comment); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/dsc/DefaultNestedDocumentHandler.java b/src/main/java/org/apache/xmlgraphics/ps/dsc/DefaultNestedDocumentHandler.java new file mode 100644 index 0000000..475958b --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/dsc/DefaultNestedDocumentHandler.java @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DefaultNestedDocumentHandler.java 1614048 2014-07-28 15:16:47Z vhennebert $ */ + +package org.apache.xmlgraphics.ps.dsc; + +import java.io.IOException; + +import org.apache.xmlgraphics.ps.DSCConstants; +import org.apache.xmlgraphics.ps.PSGenerator; +import org.apache.xmlgraphics.ps.dsc.events.DSCComment; +import org.apache.xmlgraphics.ps.dsc.events.DSCEvent; + +/** + * {@link DSCListener} implementation which automatically skips data + * between Begin/EndDocument and Begin/EndData. + */ +public class DefaultNestedDocumentHandler implements DSCParserConstants, + NestedDocumentHandler, DSCListener { + + private PSGenerator gen; + + /** + * Creates a new instance. + * @param gen PSGenerator to pass through the skipped content + */ + public DefaultNestedDocumentHandler(PSGenerator gen) { + this.gen = gen; + } + + /** {@inheritDoc} */ + public void handle(DSCEvent event, DSCParser parser) throws IOException, DSCException { + processEvent(event, parser); + } + + /** {@inheritDoc} */ + public void processEvent(DSCEvent event, DSCParser parser) throws IOException, DSCException { + if (event.isDSCComment()) { + DSCComment comment = event.asDSCComment(); + if (DSCConstants.BEGIN_DOCUMENT.equals(comment.getName())) { + if (gen != null) { + comment.generate(gen); + } + boolean checkEOF = parser.isCheckEOF(); + parser.setCheckEOF(false); + parser.setListenersDisabled(true); + comment = parser.nextDSCComment(DSCConstants.END_DOCUMENT, gen); + if (comment == null) { + throw new DSCException("File is not DSC-compliant: Didn't find an " + + DSCConstants.END_DOCUMENT); + } + if (gen != null) { + comment.generate(gen); + } + parser.setCheckEOF(checkEOF); + parser.setListenersDisabled(false); + parser.next(); + } else if (DSCConstants.BEGIN_DATA.equals(comment.getName())) { + if (gen != null) { + comment.generate(gen); + } + boolean checkEOF = parser.isCheckEOF(); + parser.setCheckEOF(false); + parser.setListenersDisabled(true); + comment = parser.nextDSCComment(DSCConstants.END_DATA, gen); + if (comment == null) { + throw new DSCException("File is not DSC-compliant: Didn't find an " + + DSCConstants.END_DATA); + } + if (gen != null) { + comment.generate(gen); + } + parser.setCheckEOF(checkEOF); + parser.setListenersDisabled(false); + parser.next(); + } + } + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/dsc/EventRecorder.java b/src/main/java/org/apache/xmlgraphics/ps/dsc/EventRecorder.java new file mode 100644 index 0000000..a3486c0 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/dsc/EventRecorder.java @@ -0,0 +1,117 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: EventRecorder.java 1804124 2017-08-04 14:13:54Z ssteiner $ */ + +package org.apache.xmlgraphics.ps.dsc; + +import java.io.IOException; +import java.util.List; + +import org.apache.xmlgraphics.ps.dsc.events.DSCComment; + +/** + * DSCHandler implementation that records DSC events. + */ +public class EventRecorder implements DSCHandler { + + private List events = new java.util.ArrayList(); + + /** + * Replays the recorded events to a specified DSCHandler instance. + * @param handler the DSCHandler to send the recorded events to + * @throws IOException In case of an I/O error + */ + public void replay(DSCHandler handler) throws IOException { + for (Object obj : events) { + if (obj instanceof PSLine) { + handler.line(((PSLine) obj).getLine()); + } else if (obj instanceof PSComment) { + handler.comment(((PSComment) obj).getComment()); + } else if (obj instanceof DSCComment) { + handler.handleDSCComment((DSCComment) obj); + } else { + throw new IllegalStateException("Unsupported class type"); + } + } + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.DSCHandler#comment(java.lang.String) + */ + public void comment(String comment) throws IOException { + events.add(new PSComment(comment)); + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.DSCHandler#handleDSCComment( + * org.apache.xmlgraphics.ps.dsc.events.DSCComment) + */ + public void handleDSCComment(DSCComment comment) throws IOException { + events.add(comment); + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.DSCHandler#line(java.lang.String) + */ + public void line(String line) throws IOException { + events.add(new PSLine(line)); + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.DSCHandler#startDocument(java.lang.String) + */ + public void startDocument(String header) throws IOException { + throw new UnsupportedOperationException( + getClass().getName() + " is only used to handle parts of a document"); + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.DSCHandler#endDocument() + */ + public void endDocument() throws IOException { + throw new UnsupportedOperationException( + getClass().getName() + " is only used to handle parts of a document"); + } + + private static class PSComment { + + private String comment; + + public PSComment(String comment) { + this.comment = comment; + } + + public String getComment() { + return this.comment; + } + } + + private static class PSLine { + + private String line; + + public PSLine(String line) { + this.line = line; + } + + public String getLine() { + return this.line; + } + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/dsc/FilteringEventListener.java b/src/main/java/org/apache/xmlgraphics/ps/dsc/FilteringEventListener.java new file mode 100644 index 0000000..af3e7f2 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/dsc/FilteringEventListener.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: FilteringEventListener.java 727407 2008-12-17 15:05:45Z jeremias $ */ + +package org.apache.xmlgraphics.ps.dsc; + +import java.io.IOException; + +import org.apache.xmlgraphics.ps.dsc.events.DSCEvent; + +/** + * {@code DSCListener} implementation that filters certain DSC events. + */ +public class FilteringEventListener implements DSCListener { + + private DSCFilter filter; + + /** + * Main constructor. + * @param filter the filter + */ + public FilteringEventListener(DSCFilter filter) { + this.filter = filter; + } + + /** {@inheritDoc} */ + public void processEvent(DSCEvent event, DSCParser parser) throws IOException, DSCException { + if (!filter.accept(event)) { + parser.next(); //skip + } + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/dsc/NestedDocumentHandler.java b/src/main/java/org/apache/xmlgraphics/ps/dsc/NestedDocumentHandler.java new file mode 100644 index 0000000..969db8b --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/dsc/NestedDocumentHandler.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: NestedDocumentHandler.java 1345683 2012-06-03 14:50:33Z gadams $ */ + +package org.apache.xmlgraphics.ps.dsc; + +import java.io.IOException; + +import org.apache.xmlgraphics.ps.dsc.events.DSCEvent; + +/** + * Interface that is used to delegate the handling of nested documents (EPS files, data sections) + * in a PostScript document. The implementation receives a parser instance so it can step forward + * until the end of the nested document is reached at which point control is given back to the + * original consumer. + *

    + * It is suggested to use the more generally usable {@link DSCListener} instead. This + * interface may be deprecated in the future. + */ +public interface NestedDocumentHandler { + + /** + * Handle a DSC event. Implementations may issue additional calls to the DSC parser and may + * modify its state. When returning from the call, state information such as filters should + * be restored. + * @param event the DSC event to handle + * @param parser the DSC parser to work with + * @throws IOException In case of an I/O error + * @throws DSCException In case of a violation of the DSC spec + */ + void handle(DSCEvent event, DSCParser parser) throws IOException, DSCException; + +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/dsc/ResourceTracker.java b/src/main/java/org/apache/xmlgraphics/ps/dsc/ResourceTracker.java new file mode 100644 index 0000000..3fbc8c6 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/dsc/ResourceTracker.java @@ -0,0 +1,261 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ResourceTracker.java 1804124 2017-08-04 14:13:54Z ssteiner $ */ + +package org.apache.xmlgraphics.ps.dsc; + +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +import org.apache.xmlgraphics.ps.PSGenerator; +import org.apache.xmlgraphics.ps.PSResource; +import org.apache.xmlgraphics.ps.dsc.events.DSCCommentDocumentNeededResources; +import org.apache.xmlgraphics.ps.dsc.events.DSCCommentDocumentSuppliedResources; +import org.apache.xmlgraphics.ps.dsc.events.DSCCommentPageResources; + +/** + * This class is used to track resources in a DSC-compliant PostScript file. The distinction is + * made between supplied and needed resources. For the details of this distinction, please see + * the DSC specification. + */ +public class ResourceTracker { + + private Set documentSuppliedResources; + private Set documentNeededResources; + private Set usedResources; + private Set pageResources; + + //Map + private Map resourceUsageCounts; + + /** + * Returns the set of supplied resources. + * @return the set of supplied resources + */ + public Set getDocumentSuppliedResources() { + if (documentSuppliedResources != null) { + return Collections.unmodifiableSet(documentSuppliedResources); + } else { + return Collections.EMPTY_SET; + } + } + + /** + * Returns the set of needed resources. + * @return the set of needed resources + */ + public Set getDocumentNeededResources() { + if (documentNeededResources != null) { + return Collections.unmodifiableSet(documentNeededResources); + } else { + return Collections.EMPTY_SET; + } + } + + /** + * Notifies the resource tracker that a new page has been started and that the page resource + * set can be cleared. + */ + public void notifyStartNewPage() { + if (pageResources != null) { + pageResources.clear(); + } + } + + /** + * Registers a supplied resource. If the same resources is already in the set of needed + * resources, it is removed there. + * @param res the resource + */ + public void registerSuppliedResource(PSResource res) { + if (documentSuppliedResources == null) { + documentSuppliedResources = new java.util.HashSet(); + } + documentSuppliedResources.add(res); + if (documentNeededResources != null) { + documentNeededResources.remove(res); + } + } + + /** + * Registers a needed resource. If the same resources is already in the set of supplied + * resources, it is ignored, i.e. it is assumed to be supplied. + * @param res the resource + */ + public void registerNeededResource(PSResource res) { + if (documentSuppliedResources == null || !documentSuppliedResources.contains(res)) { + if (documentNeededResources == null) { + documentNeededResources = new java.util.HashSet(); + } + documentNeededResources.add(res); + } + } + + private void preparePageResources() { + if (pageResources == null) { + pageResources = new java.util.HashSet(); + } + } + + private void prepareUsageCounts() { + if (resourceUsageCounts == null) { + resourceUsageCounts = new java.util.HashMap(); + } + } + + /** + * Notifies the resource tracker about the usage of a resource on the current page. + * @param res the resource being used + */ + public void notifyResourceUsageOnPage(PSResource res) { + preparePageResources(); + pageResources.add(res); + + prepareUsageCounts(); + Counter counter = (Counter)resourceUsageCounts.get(res); + if (counter == null) { + resourceUsageCounts.put(res, new Counter()); + } else { + counter.inc(); + } + } + + /** + * Notifies the resource tracker about the usage of resources on the current page. + * @param resources the resources being used + */ + public void notifyResourceUsageOnPage(Collection resources) { + preparePageResources(); + for (Object resource : resources) { + PSResource res = (PSResource) resource; + notifyResourceUsageOnPage(res); + } + } + + /** + * Indicates whether a particular resource is supplied, rather than needed. + * @param res the resource + * @return true if the resource is registered as being supplied. + */ + public boolean isResourceSupplied(PSResource res) { + return (documentSuppliedResources != null) && documentSuppliedResources.contains(res); + } + + /** + * Writes a DSC comment for the accumulated used resources, either at page level or + * at document level. + * @param pageLevel true if the DSC comment for the page level should be generated, + * false for the document level (in the trailer) + * @param gen the PSGenerator to write the DSC comments with + * @exception IOException In case of an I/O problem + */ + public void writeResources(boolean pageLevel, PSGenerator gen) throws IOException { + if (pageLevel) { + writePageResources(gen); + } else { + writeDocumentResources(gen); + } + } + + /** + * Writes a DSC comment for the accumulated used resources on the current page. Then it commits + * all those resources to the used resources on document level. + * @param gen the PSGenerator to write the DSC comments with + * @exception IOException In case of an I/O problem + */ + public void writePageResources(PSGenerator gen) throws IOException { + new DSCCommentPageResources(pageResources).generate(gen); + if (usedResources == null) { + usedResources = new java.util.HashSet(); + } + usedResources.addAll(pageResources); + } + + /** + * Writes a DSC comment for the needed and supplied resourced for the current DSC document. + * @param gen the PSGenerator to write the DSC comments with + * @exception IOException In case of an I/O problem + */ + public void writeDocumentResources(PSGenerator gen) throws IOException { + if (usedResources != null) { + for (Object usedResource : usedResources) { + PSResource res = (PSResource) usedResource; + if (documentSuppliedResources == null + || !documentSuppliedResources.contains(res)) { + registerNeededResource(res); + } + } + } + new DSCCommentDocumentNeededResources(documentNeededResources).generate(gen); + new DSCCommentDocumentSuppliedResources(documentSuppliedResources).generate(gen); + } + + /** + * This method declares that the given resource will be inlined and can therefore + * be removed from resource tracking. This is useful when you don't know beforehand + * if a resource will be used multiple times. If it's only used once it's better + * to inline the resource to lower the maximum memory needed inside the PostScript + * interpreter. + * @param res the resource + */ + public void declareInlined(PSResource res) { + if (this.documentNeededResources != null) { + this.documentNeededResources.remove(res); + } + if (this.documentSuppliedResources != null) { + this.documentSuppliedResources.remove(res); + } + if (this.pageResources != null) { + this.pageResources.remove(res); + } + if (this.usedResources != null) { + this.usedResources.remove(res); + } + } + + /** + * Returns the number of times a resource has been used inside the current DSC document. + * @param res the resource + * @return the number of times the resource has been used + */ + public long getUsageCount(PSResource res) { + Counter counter = (Counter)resourceUsageCounts.get(res); + return (counter != null ? counter.getCount() : 0); + } + + private static class Counter { + + private long count = 1; + + public void inc() { + this.count++; + } + + public long getCount() { + return this.count; + } + + public String toString() { + return Long.toString(this.count); + } + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/dsc/events/AbstractDSCComment.java b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/AbstractDSCComment.java new file mode 100644 index 0000000..4f82338 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/AbstractDSCComment.java @@ -0,0 +1,157 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: AbstractDSCComment.java 1345683 2012-06-03 14:50:33Z gadams $ */ + +package org.apache.xmlgraphics.ps.dsc.events; + +import java.util.List; + +/** + * Abstract base class for DSC comments. + */ +public abstract class AbstractDSCComment extends AbstractEvent implements DSCComment { + + private boolean isWhitespace(char c) { + return c == ' ' || c == '\t'; + } + + private int parseNextParam(String value, int pos, List lst) { + int startPos = pos; + pos++; + while (pos < value.length() && !isWhitespace(value.charAt(pos))) { + pos++; + } + String param = value.substring(startPos, pos); + lst.add(param); + return pos; + } + + private int parseNextParentheseString(String value, int pos, List lst) { + int nestLevel = 1; + pos++; + StringBuffer sb = new StringBuffer(); + while (pos < value.length() && nestLevel > 0) { + final char c = value.charAt(pos); + switch (c) { + case '(': + nestLevel++; + if (nestLevel > 1) { + sb.append(c); + } + break; + case ')': + if (nestLevel > 1) { + sb.append(c); + } + nestLevel--; + break; + case '\\': + pos++; + char cnext = value.charAt(pos); + switch (cnext) { + case '\\': + sb.append(cnext); + break; + case 'n': + sb.append('\n'); + break; + case 'r': + sb.append('\r'); + break; + case 't': + sb.append('\t'); + break; + case 'b': + sb.append('\b'); + break; + case 'f': + sb.append('\f'); + break; + case '(': + sb.append('('); + break; + case ')': + sb.append(')'); + break; + default: + int code = Integer.parseInt(value.substring(pos, pos + 3), 8); + sb.append((char)code); + pos += 2; + } + break; + default: + sb.append(c); + } + pos++; + } + lst.add(sb.toString()); + pos++; + return pos; + } + + /** + * Splits the params of the DSC comment value in to a List. + * @param value the DSC comment value + * @return the List of values + */ + protected List splitParams(String value) { + List lst = new java.util.ArrayList(); + int pos = 0; + value = value.trim(); + while (pos < value.length()) { + if (isWhitespace(value.charAt(pos))) { + pos++; + continue; + } + if (value.charAt(pos) == '(') { + pos = parseNextParentheseString(value, pos, lst); + } else { + pos = parseNextParam(value, pos, lst); + } + } + return lst; + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.DSCComment#isAtend() + */ + public boolean isAtend() { + return false; + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.AbstractEvent#asDSCComment() + */ + public DSCComment asDSCComment() { + return this; + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.AbstractEvent#isDSCComment() + */ + public boolean isDSCComment() { + return true; + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.DSCEvent#getEventType() + */ + public int getEventType() { + return DSC_COMMENT; + } +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/dsc/events/AbstractEvent.java b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/AbstractEvent.java new file mode 100644 index 0000000..293c1e1 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/AbstractEvent.java @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: AbstractEvent.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.ps.dsc.events; + +/** + * Abstract base class for DSC events. + */ +public abstract class AbstractEvent implements DSCEvent { + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.DSCEvent#isComment() + */ + public boolean isComment() { + return false; + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.DSCEvent#isDSCComment() + */ + public boolean isDSCComment() { + return false; + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.DSCEvent#isHeaderComment() + */ + public boolean isHeaderComment() { + return false; + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.DSCEvent#isLine() + */ + public boolean isLine() { + return false; + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.DSCEvent#asDSCComment() + */ + public DSCComment asDSCComment() { + throw new ClassCastException(this.getClass().getName()); + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.DSCEvent#asLine() + */ + public PostScriptLine asLine() { + throw new ClassCastException(this.getClass().getName()); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/dsc/events/AbstractResourceDSCComment.java b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/AbstractResourceDSCComment.java new file mode 100644 index 0000000..aff7689 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/AbstractResourceDSCComment.java @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: AbstractResourceDSCComment.java 727407 2008-12-17 15:05:45Z jeremias $ */ + +package org.apache.xmlgraphics.ps.dsc.events; + +import java.io.IOException; +import java.util.Iterator; +import java.util.List; + +import org.apache.xmlgraphics.ps.PSGenerator; +import org.apache.xmlgraphics.ps.PSProcSet; +import org.apache.xmlgraphics.ps.PSResource; + +/** + * Abstract base class for resource comments. + */ +public abstract class AbstractResourceDSCComment extends AbstractDSCComment { + + private PSResource resource; + + /** + * Creates a new instance + */ + public AbstractResourceDSCComment() { + } + + /** + * Creates a new instance for a given PSResource instance + * @param resource the resource + */ + public AbstractResourceDSCComment(PSResource resource) { + this.resource = resource; + } + + /** + * Returns the associated PSResource. + * @return the resource + */ + public PSResource getResource() { + return this.resource; + } + + /** {@inheritDoc} */ + public boolean hasValues() { + return true; + } + + /** {@inheritDoc} */ + public void parseValue(String value) { + List params = splitParams(value); + Iterator iter = params.iterator(); + String name = (String)iter.next(); + if (PSResource.TYPE_FONT.equals(name)) { + String fontname = (String)iter.next(); + this.resource = new PSResource(name, fontname); + } else if (PSResource.TYPE_PROCSET.equals(name)) { + String procname = (String)iter.next(); + String version = (String)iter.next(); + String revision = (String)iter.next(); + this.resource = new PSProcSet(procname, + Float.parseFloat(version), Integer.parseInt(revision)); + } else if (PSResource.TYPE_FILE.equals(name)) { + String filename = (String)iter.next(); + this.resource = new PSResource(name, filename); + } else if (PSResource.TYPE_FORM.equals(name)) { + String formname = (String)iter.next(); + this.resource = new PSResource(name, formname); + } else if (PSResource.TYPE_PATTERN.equals(name)) { + String patternname = (String)iter.next(); + this.resource = new PSResource(name, patternname); + } else if (PSResource.TYPE_ENCODING.equals(name)) { + String encodingname = (String)iter.next(); + this.resource = new PSResource(name, encodingname); + } else { + throw new IllegalArgumentException("Invalid resource type: " + name); + } + } + + /** {@inheritDoc} */ + public void generate(PSGenerator gen) throws IOException { + gen.writeDSCComment(getName(), getResource()); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/dsc/events/AbstractResourcesDSCComment.java b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/AbstractResourcesDSCComment.java new file mode 100644 index 0000000..203b2a9 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/AbstractResourcesDSCComment.java @@ -0,0 +1,165 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: AbstractResourcesDSCComment.java 1804124 2017-08-04 14:13:54Z ssteiner $ */ + +package org.apache.xmlgraphics.ps.dsc.events; + +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import org.apache.xmlgraphics.ps.PSGenerator; +import org.apache.xmlgraphics.ps.PSProcSet; +import org.apache.xmlgraphics.ps.PSResource; + +/** + * Abstract base class for Resource DSC comments (DocumentNeededResources, + * DocumentSuppliedResources and PageResources). + */ +public abstract class AbstractResourcesDSCComment extends AbstractDSCComment { + + private Set resources; + + /** + * Creates a new instance. + */ + public AbstractResourcesDSCComment() { + super(); + } + + /** + * Creates a new instance. + * @param resources a Collection of PSResource instances + */ + public AbstractResourcesDSCComment(Collection resources) { + addResources(resources); + } + + /** {@inheritDoc} */ + public boolean hasValues() { + return true; + } + + private void prepareResourceSet() { + if (this.resources == null) { + this.resources = new java.util.TreeSet(); + } + } + + /** + * Adds a new resource. + * @param res the resource + */ + public void addResource(PSResource res) { + prepareResourceSet(); + this.resources.add(res); + } + + /** + * Adds a collection of resources. + * @param resources a Collection of PSResource instances. + */ + public void addResources(Collection resources) { + if (resources != null) { + prepareResourceSet(); + this.resources.addAll(resources); + } + } + + /** + * Returns the set of resources associated with this DSC comment. + * @return the set of resources + */ + public Set getResources() { + return Collections.unmodifiableSet(this.resources); + } + + /** + * Defines the known resource types (font, procset, file, pattern etc.). + */ + static final Set RESOURCE_TYPES = new java.util.HashSet(); + + static { + RESOURCE_TYPES.add(PSResource.TYPE_FONT); + RESOURCE_TYPES.add(PSResource.TYPE_PROCSET); + RESOURCE_TYPES.add(PSResource.TYPE_FILE); + RESOURCE_TYPES.add(PSResource.TYPE_PATTERN); + RESOURCE_TYPES.add(PSResource.TYPE_FORM); + RESOURCE_TYPES.add(PSResource.TYPE_ENCODING); + } + + /** {@inheritDoc} */ + public void parseValue(String value) { + List params = splitParams(value); + String currentResourceType = null; + Iterator iter = params.iterator(); + while (iter.hasNext()) { + String name = (String)iter.next(); + if (RESOURCE_TYPES.contains(name)) { + currentResourceType = name; + } + if (currentResourceType == null) { + throw new IllegalArgumentException( + " must begin with a resource type. Found: " + name); + } + if (PSResource.TYPE_FONT.equals(currentResourceType)) { + String fontname = (String)iter.next(); + addResource(new PSResource(name, fontname)); + } else if (PSResource.TYPE_FORM.equals(currentResourceType)) { + String formname = (String)iter.next(); + addResource(new PSResource(name, formname)); + } else if (PSResource.TYPE_PROCSET.equals(currentResourceType)) { + String procname = (String)iter.next(); + String version = (String)iter.next(); + String revision = (String)iter.next(); + addResource(new PSProcSet(procname, + Float.parseFloat(version), Integer.parseInt(revision))); + } else if (PSResource.TYPE_FILE.equals(currentResourceType)) { + String filename = (String)iter.next(); + addResource(new PSResource(name, filename)); + } else { + throw new IllegalArgumentException("Invalid resource type: " + currentResourceType); + } + } + } + + /** {@inheritDoc} */ + public void generate(PSGenerator gen) throws IOException { + if (resources == null || resources.size() == 0) { + return; + } + StringBuffer sb = new StringBuffer(); + sb.append("%%").append(getName()).append(": "); + boolean first = true; + for (Object resource : resources) { + if (!first) { + gen.writeln(sb.toString()); + sb.setLength(0); + sb.append("%%+ "); + } + PSResource res = (PSResource) resource; + sb.append(res.getResourceSpecification()); + first = false; + } + gen.writeln(sb.toString()); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCAtend.java b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCAtend.java new file mode 100644 index 0000000..fb23c62 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCAtend.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DSCAtend.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.ps.dsc.events; + +import java.io.IOException; + +import org.apache.xmlgraphics.ps.DSCConstants; +import org.apache.xmlgraphics.ps.PSGenerator; +import org.apache.xmlgraphics.ps.dsc.DSCCommentFactory; + +/** + * This class represents a DSC comment with an "(ATEND)" value. + */ +public class DSCAtend extends AbstractDSCComment { + + private String name; + + /** + * Creates a new instance + * @param name the name of the DSC comment (without the "%%" prefix) + */ + public DSCAtend(String name) { + this.name = name; + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.DSCComment#getName() + */ + public String getName() { + return this.name; + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.DSCComment#hasValues() + */ + public boolean hasValues() { + return false; + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.AbstractDSCComment#isAtend() + */ + public boolean isAtend() { + return true; + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.DSCComment#parseValue(java.lang.String) + */ + public void parseValue(String value) { + //nop + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.DSCEvent#generate( + * org.apache.xmlgraphics.ps.PSGenerator) + */ + public void generate(PSGenerator gen) throws IOException { + gen.writeDSCComment(getName(), DSCConstants.ATEND); + } + + /** + * Creates a new instance of a DSC comment with the same name as this instance but does not + * have an "Atend" value. + * @return the new DSC comment + */ + public DSCComment createDSCCommentFromAtend() { + return DSCCommentFactory.createDSCCommentFor(getName()); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCComment.java b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCComment.java new file mode 100644 index 0000000..dec2f7d --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCComment.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DSCComment.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.ps.dsc.events; + +import java.io.IOException; + +import org.apache.xmlgraphics.ps.PSGenerator; + +/** + * Base interface for all DSC comments. + */ +public interface DSCComment extends DSCEvent { + + /** + * Returns the name of the DSC comment. + * @return the name of the DSC comment (without the "%%" prefix) + */ + String getName(); + + /** + * Parses the value of the DSC comment. + * @param value the value + */ + void parseValue(String value); + + /** + * Indicates whether this DSC comment has values. + * @return true if the DSC comment has values + */ + boolean hasValues(); + + /** + * Indicates whether the DSC comment's value is "Atend". + * @return true if the value is "Atend" + */ + boolean isAtend(); + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.DSCEvent#generate( + * org.apache.xmlgraphics.ps.PSGenerator) + */ + void generate(PSGenerator gen) throws IOException; + +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCCommentBeginDocument.java b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCCommentBeginDocument.java new file mode 100644 index 0000000..d641981 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCCommentBeginDocument.java @@ -0,0 +1,131 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DSCCommentBeginDocument.java 1681108 2015-05-22 13:26:12Z ssteiner $ */ + +package org.apache.xmlgraphics.ps.dsc.events; + +import java.io.IOException; +import java.util.Iterator; +import java.util.List; + +import org.apache.xmlgraphics.ps.DSCConstants; +import org.apache.xmlgraphics.ps.PSGenerator; +import org.apache.xmlgraphics.ps.PSResource; + +/** + * Represents a %BeginDocument DSC comment. + */ +public class DSCCommentBeginDocument extends AbstractDSCComment { + + private PSResource resource; + private Float version; + private String type; + + /** + * Creates a new instance + */ + public DSCCommentBeginDocument() { + super(); + } + + /** + * Creates a new instance for a given PSResource instance + * @param resource the resource + */ + public DSCCommentBeginDocument(PSResource resource) { + this.resource = resource; + if (resource != null && !PSResource.TYPE_FILE.equals(resource.getType())) { + throw new IllegalArgumentException("Resource must be of type 'file'"); + } + } + + /** + * Creates a new instance for a given PSResource instance + * @param resource the resource + * @param version the version of the resource (or null) + * @param type the type of resource (or null) + */ + public DSCCommentBeginDocument(PSResource resource, Float version, String type) { + this(resource); + this.version = version; + this.type = type; + } + + /** + * Returns the resource version. + * @return the resource version (or null if not applicable) + */ + public Float getVersion() { + return this.version; + } + + /** + * Returns the resource type + * @return the resource type (or null if not applicable) + */ + public String getType() { + return this.type; + } + + /** {@inheritDoc} */ + public String getName() { + return DSCConstants.BEGIN_DOCUMENT; + } + + /** + * Returns the associated PSResource. + * @return the resource + */ + public PSResource getResource() { + return this.resource; + } + + /** {@inheritDoc} */ + public boolean hasValues() { + return true; + } + + /** {@inheritDoc} */ + public void parseValue(String value) { + List params = splitParams(value); + Iterator iter = params.iterator(); + String name = (String)iter.next(); + this.resource = new PSResource(PSResource.TYPE_FILE, name); + if (iter.hasNext()) { + this.version = Float.valueOf(iter.next().toString()); + this.type = null; + if (iter.hasNext()) { + this.type = (String)iter.next(); + } + } + } + + /** {@inheritDoc} */ + public void generate(PSGenerator gen) throws IOException { + List params = new java.util.ArrayList(); + params.add(getResource().getName()); + if (getVersion() != null) { + params.add(getVersion()); + if (getType() != null) { + params.add(getType()); + } + } + gen.writeDSCComment(getName(), params.toArray()); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCCommentBeginResource.java b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCCommentBeginResource.java new file mode 100644 index 0000000..b59f77d --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCCommentBeginResource.java @@ -0,0 +1,94 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DSCCommentBeginResource.java 1681108 2015-05-22 13:26:12Z ssteiner $ */ + +package org.apache.xmlgraphics.ps.dsc.events; + +import java.io.IOException; + +import org.apache.xmlgraphics.ps.DSCConstants; +import org.apache.xmlgraphics.ps.PSGenerator; +import org.apache.xmlgraphics.ps.PSResource; + +/** + * Represents a %BeginResource DSC comment. + */ +public class DSCCommentBeginResource extends AbstractResourceDSCComment { + + private Integer min; + private Integer max; + + /** + * Creates a new instance + */ + public DSCCommentBeginResource() { + super(); + } + + /** + * Creates a new instance for a given PSResource instance + * @param resource the resource + */ + public DSCCommentBeginResource(PSResource resource) { + super(resource); + } + + /** + * Creates a new instance for a given PSResource instance + * @param resource the resource + * @param min Minimum VM used by the resource + * @param max Maximum VM used by the resource + */ + public DSCCommentBeginResource(PSResource resource, int min, int max) { + super(resource); + this.min = min; + this.max = max; + } + + /** + * Returns the minimum VM used by the resource. + * @return the minimum VM used by the resource + */ + public Integer getMin() { + return this.min; + } + + /** + * Returns the maximum VM used by the resource. + * @return the maximum VM used by the resource + */ + public Integer getMax() { + return this.max; + } + + /** {@inheritDoc} */ + public String getName() { + return DSCConstants.BEGIN_RESOURCE; + } + + /** {@inheritDoc} */ + public void generate(PSGenerator gen) throws IOException { + if (getMin() != null) { + Object[] params = new Object[] {getResource(), getMin(), getMax()}; + gen.writeDSCComment(getName(), params); + } else { + super.generate(gen); + } + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCCommentBoundingBox.java b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCCommentBoundingBox.java new file mode 100644 index 0000000..0efb969 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCCommentBoundingBox.java @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DSCCommentBoundingBox.java 1681108 2015-05-22 13:26:12Z ssteiner $ */ + +package org.apache.xmlgraphics.ps.dsc.events; + +import java.awt.geom.Rectangle2D; +import java.io.IOException; +import java.util.Iterator; +import java.util.List; + +import org.apache.xmlgraphics.ps.DSCConstants; +import org.apache.xmlgraphics.ps.PSGenerator; + +/** + * Represents a %%BoundingBox DSC comment. + */ +public class DSCCommentBoundingBox extends AbstractDSCComment { + + private Rectangle2D bbox; + + /** + * Creates a new instance. + */ + public DSCCommentBoundingBox() { + } + + /** + * Creates a new instance. + * @param bbox the bounding box + */ + public DSCCommentBoundingBox(Rectangle2D bbox) { + setBoundingBox(bbox); + } + + /** + * Returns the bounding box. + * @return the bounding box + */ + public Rectangle2D getBoundingBox() { + return this.bbox; + } + + /** + * Sets the bounding box. + * @param bbox the bounding box + */ + public void setBoundingBox(Rectangle2D bbox) { + this.bbox = bbox; + } + + /** {@inheritDoc} */ + public String getName() { + return DSCConstants.BBOX; + } + + /** {@inheritDoc} */ + public boolean hasValues() { + return true; + } + + /** {@inheritDoc} */ + public void parseValue(String value) { + List params = splitParams(value); + Iterator iter = params.iterator(); + + double x1 = Double.parseDouble((String)iter.next()); + double y1 = Double.parseDouble((String)iter.next()); + double x2 = Double.parseDouble((String)iter.next()); + double y2 = Double.parseDouble((String)iter.next()); + this.bbox = new Rectangle2D.Double(x1, y1, x2 - x1, y2 - y1); + } + + /** {@inheritDoc} */ + public void generate(PSGenerator gen) throws IOException { + if (getBoundingBox() != null) { + gen.writeDSCComment(getName(), new Object[] { + (int) Math.floor(this.bbox.getX()), + (int) Math.floor(this.bbox.getY()), + (int) Math.ceil(this.bbox.getX() + this.bbox.getWidth()), + (int) Math.ceil(this.bbox.getY() + this.bbox.getHeight())}); + } else { + gen.writeDSCComment(getName(), DSCConstants.ATEND); + } + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCCommentDocumentNeededResources.java b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCCommentDocumentNeededResources.java new file mode 100644 index 0000000..0de2588 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCCommentDocumentNeededResources.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DSCCommentDocumentNeededResources.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.ps.dsc.events; + +import java.util.Collection; + +import org.apache.xmlgraphics.ps.DSCConstants; + +/** + * Represents a %%DocumentNeededResources DSC comment. + */ +public class DSCCommentDocumentNeededResources extends AbstractResourcesDSCComment { + + /** + * Creates a new instance. + */ + public DSCCommentDocumentNeededResources() { + super(); + } + + /** + * Creates a new instance. + * @param resources a Collection of PSResource instances + */ + public DSCCommentDocumentNeededResources(Collection resources) { + super(resources); + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.DSCComment#getName() + */ + public String getName() { + return DSCConstants.DOCUMENT_NEEDED_RESOURCES; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCCommentDocumentSuppliedResources.java b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCCommentDocumentSuppliedResources.java new file mode 100644 index 0000000..2c480da --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCCommentDocumentSuppliedResources.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DSCCommentDocumentSuppliedResources.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.ps.dsc.events; + +import java.util.Collection; + +import org.apache.xmlgraphics.ps.DSCConstants; + +/** + * Represents a %%DocumentSuppliedResources DSC comment. + */ +public class DSCCommentDocumentSuppliedResources extends AbstractResourcesDSCComment { + + /** + * Creates a new instance. + */ + public DSCCommentDocumentSuppliedResources() { + super(); + } + + /** + * Creates a new instance. + * @param resources a Collection of PSResource instances + */ + public DSCCommentDocumentSuppliedResources(Collection resources) { + super(resources); + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.DSCComment#getName() + */ + public String getName() { + return DSCConstants.DOCUMENT_SUPPLIED_RESOURCES; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCCommentEndComments.java b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCCommentEndComments.java new file mode 100644 index 0000000..90e5c6b --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCCommentEndComments.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DSCCommentEndComments.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.ps.dsc.events; + +import java.io.IOException; + +import org.apache.xmlgraphics.ps.DSCConstants; +import org.apache.xmlgraphics.ps.PSGenerator; + +/** + * Respresents a %%EndComments DSC comment. + */ +public class DSCCommentEndComments extends AbstractDSCComment { + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.DSCComment#getName() + */ + public String getName() { + return DSCConstants.END_COMMENTS; + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.DSCComment#hasValues() + */ + public boolean hasValues() { + return false; + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.DSCComment#parseValue(java.lang.String) + */ + public void parseValue(String value) { + //nop + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.DSCEvent#generate( + * org.apache.xmlgraphics.ps.PSGenerator) + */ + public void generate(PSGenerator gen) throws IOException { + gen.writeDSCComment(getName()); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCCommentEndOfFile.java b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCCommentEndOfFile.java new file mode 100644 index 0000000..d1ae183 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCCommentEndOfFile.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DSCCommentEndOfFile.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.ps.dsc.events; + +import java.io.IOException; + +import org.apache.xmlgraphics.ps.DSCConstants; +import org.apache.xmlgraphics.ps.PSGenerator; + +/** + * Represents a %%EOF DSC comment. + */ +public class DSCCommentEndOfFile extends AbstractDSCComment { + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.DSCComment#getName() + */ + public String getName() { + return DSCConstants.EOF; + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.DSCComment#hasValues() + */ + public boolean hasValues() { + return false; + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.DSCComment#parseValue(java.lang.String) + */ + public void parseValue(String value) { + //nop + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.DSCEvent#generate(org.apache.xmlgraphics.ps.PSGenerator) + */ + public void generate(PSGenerator gen) throws IOException { + gen.writeDSCComment(getName()); + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.AbstractDSCComment#getEventType() + */ + public int getEventType() { + return EOF; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCCommentHiResBoundingBox.java b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCCommentHiResBoundingBox.java new file mode 100644 index 0000000..a78af59 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCCommentHiResBoundingBox.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DSCCommentHiResBoundingBox.java 1681108 2015-05-22 13:26:12Z ssteiner $ */ + +package org.apache.xmlgraphics.ps.dsc.events; + +import java.awt.geom.Rectangle2D; +import java.io.IOException; + +import org.apache.xmlgraphics.ps.DSCConstants; +import org.apache.xmlgraphics.ps.PSGenerator; + +/** + * Represents a %%HiResBoundingBox DSC comment. + */ +public class DSCCommentHiResBoundingBox extends DSCCommentBoundingBox { + + /** + * Creates a new instance. + */ + public DSCCommentHiResBoundingBox() { + super(); + } + + /** + * Creates a new instance. + * @param bbox the bounding box + */ + public DSCCommentHiResBoundingBox(Rectangle2D bbox) { + super(bbox); + } + + /** {@inheritDoc} */ + public String getName() { + return DSCConstants.HIRES_BBOX; + } + + /** {@inheritDoc} */ + public void generate(PSGenerator gen) throws IOException { + if (getBoundingBox() != null) { + gen.writeDSCComment(getName(), new Object[] { + getBoundingBox().getX(), + getBoundingBox().getY(), + getBoundingBox().getX() + getBoundingBox().getWidth(), + getBoundingBox().getY() + getBoundingBox().getHeight()}); + } else { + gen.writeDSCComment(getName(), DSCConstants.ATEND); + } + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCCommentIncludeResource.java b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCCommentIncludeResource.java new file mode 100644 index 0000000..33840c8 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCCommentIncludeResource.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DSCCommentIncludeResource.java 727407 2008-12-17 15:05:45Z jeremias $ */ + +package org.apache.xmlgraphics.ps.dsc.events; + +import org.apache.xmlgraphics.ps.DSCConstants; +import org.apache.xmlgraphics.ps.PSResource; + +/** + * Represents a %IncludeResource DSC comment. + */ +public class DSCCommentIncludeResource extends AbstractResourceDSCComment { + + /** + * Creates a new instance + */ + public DSCCommentIncludeResource() { + super(); + } + + /** + * Creates a new instance for a given PSResource instance + * @param resource the resource + */ + public DSCCommentIncludeResource(PSResource resource) { + super(resource); + } + + /** {@inheritDoc} */ + public String getName() { + return DSCConstants.INCLUDE_RESOURCE; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCCommentLanguageLevel.java b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCCommentLanguageLevel.java new file mode 100644 index 0000000..8e262f4 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCCommentLanguageLevel.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DSCCommentLanguageLevel.java 1681108 2015-05-22 13:26:12Z ssteiner $ */ + +package org.apache.xmlgraphics.ps.dsc.events; + +import java.io.IOException; + +import org.apache.xmlgraphics.ps.DSCConstants; +import org.apache.xmlgraphics.ps.PSGenerator; + +/** + * Represents a %%LanguageLevel DSC comment + */ +public class DSCCommentLanguageLevel extends AbstractDSCComment { + + private int level; + + /** + * Creates a new instance. + */ + public DSCCommentLanguageLevel() { + } + + /** + * Creates a new instance + * @param level the PostScript language level (usually 2 or 3) + */ + public DSCCommentLanguageLevel(int level) { + this.level = level; + } + + /** + * Returns the PostScript language level (usually 2 or 3). + * @return the language level + */ + public int getLanguageLevel() { + return this.level; + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.DSCComment#getName() + */ + public String getName() { + return DSCConstants.LANGUAGE_LEVEL; + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.DSCComment#hasValues() + */ + public boolean hasValues() { + return true; + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.DSCComment#parseValue(java.lang.String) + */ + public void parseValue(String value) { + this.level = Integer.parseInt(value); + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.DSCEvent#generate(org.apache.xmlgraphics.ps.PSGenerator) + */ + public void generate(PSGenerator gen) throws IOException { + if (level <= 0) { + throw new IllegalStateException("Language Level was not properly set"); + } + gen.writeDSCComment(getName(), getLanguageLevel()); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCCommentPage.java b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCCommentPage.java new file mode 100644 index 0000000..cd1ae69 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCCommentPage.java @@ -0,0 +1,129 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DSCCommentPage.java 1681108 2015-05-22 13:26:12Z ssteiner $ */ + +package org.apache.xmlgraphics.ps.dsc.events; + +import java.io.IOException; +import java.util.Iterator; +import java.util.List; + +import org.apache.xmlgraphics.ps.DSCConstants; +import org.apache.xmlgraphics.ps.PSGenerator; + +/** + * Represents a %%Page DSC comment. + */ +public class DSCCommentPage extends AbstractDSCComment { + + private String pageName; + private int pagePosition = -1; + + /** + * Creates a new instance. + */ + public DSCCommentPage() { + } + + /** + * Creates a new instance. + * @param pageName the name of the page + * @param pagePosition the position of the page within the file (1-based) + */ + public DSCCommentPage(String pageName, int pagePosition) { + setPageName(pageName); + setPagePosition(pagePosition); + } + + /** + * Creates a new instance. The page name will be set to the same value as the page position. + * @param pagePosition the position of the page within the file (1-based) + */ + public DSCCommentPage(int pagePosition) { + this(Integer.toString(pagePosition), pagePosition); + } + + /** + * Returns the name of the page. + * @return the page name + */ + public String getPageName() { + return this.pageName; + } + + /** + * Sets the page name. + * @param name the page name + */ + public void setPageName(String name) { + this.pageName = name; + } + + /** + * Returns the page position. + * @return the page position (1-based) + */ + public int getPagePosition() { + return this.pagePosition; + } + + /** + * Sets the page position. + * @param position the page position (1-based) + */ + public void setPagePosition(int position) { + if (position <= 0) { + throw new IllegalArgumentException("position must be 1 or above"); + } + this.pagePosition = position; + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.DSCComment#getName() + */ + public String getName() { + return DSCConstants.PAGE; + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.DSCComment#hasValues() + */ + public boolean hasValues() { + return true; + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.DSCComment#parseValue(java.lang.String) + */ + public void parseValue(String value) { + List params = splitParams(value); + Iterator iter = params.iterator(); + this.pageName = (String)iter.next(); + this.pagePosition = Integer.parseInt((String)iter.next()); + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.DSCEvent#generate( + * org.apache.xmlgraphics.ps.PSGenerator) + */ + public void generate(PSGenerator gen) throws IOException { + gen.writeDSCComment(getName(), + new Object[] {getPageName(), getPagePosition()}); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCCommentPageBoundingBox.java b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCCommentPageBoundingBox.java new file mode 100644 index 0000000..3b12d3d --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCCommentPageBoundingBox.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DSCCommentPageBoundingBox.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.ps.dsc.events; + +import java.awt.geom.Rectangle2D; + +import org.apache.xmlgraphics.ps.DSCConstants; + +/** + * Represents a %%PageBoundingBox DSC comment. + */ +public class DSCCommentPageBoundingBox extends DSCCommentBoundingBox { + + /** + * Creates a new instance. + */ + public DSCCommentPageBoundingBox() { + super(); + } + + /** + * Creates a new instance. + * @param bbox the bounding box + */ + public DSCCommentPageBoundingBox(Rectangle2D bbox) { + super(bbox); + } + + /** {@inheritDoc} */ + public String getName() { + return DSCConstants.PAGE_BBOX; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCCommentPageHiResBoundingBox.java b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCCommentPageHiResBoundingBox.java new file mode 100644 index 0000000..609cc58 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCCommentPageHiResBoundingBox.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DSCCommentPageHiResBoundingBox.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.ps.dsc.events; + +import java.awt.geom.Rectangle2D; + +import org.apache.xmlgraphics.ps.DSCConstants; + +/** + * Represents a %%PageHiResBoundingBox DSC comment. + */ +public class DSCCommentPageHiResBoundingBox extends DSCCommentHiResBoundingBox { + + /** + * Creates a new instance. + */ + public DSCCommentPageHiResBoundingBox() { + super(); + } + + /** + * Creates a new instance. + * @param bbox the bounding box + */ + public DSCCommentPageHiResBoundingBox(Rectangle2D bbox) { + super(bbox); + } + + /** {@inheritDoc} */ + public String getName() { + return DSCConstants.PAGE_HIRES_BBOX; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCCommentPageResources.java b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCCommentPageResources.java new file mode 100644 index 0000000..56030b6 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCCommentPageResources.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DSCCommentPageResources.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.ps.dsc.events; + +import java.util.Collection; + +import org.apache.xmlgraphics.ps.DSCConstants; + +/** + * Represents a %%PageResources DSC comment. + */ +public class DSCCommentPageResources extends AbstractResourcesDSCComment { + + /** + * Creates a new instance. + */ + public DSCCommentPageResources() { + super(); + } + + /** + * Creates a new instance. + * @param resources a Collection of PSResource instances + */ + public DSCCommentPageResources(Collection resources) { + super(resources); + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.DSCComment#getName() + */ + public String getName() { + return DSCConstants.PAGE_RESOURCES; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCCommentPages.java b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCCommentPages.java new file mode 100644 index 0000000..edbd783 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCCommentPages.java @@ -0,0 +1,96 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DSCCommentPages.java 1681108 2015-05-22 13:26:12Z ssteiner $ */ + +package org.apache.xmlgraphics.ps.dsc.events; + +import java.io.IOException; + +import org.apache.xmlgraphics.ps.DSCConstants; +import org.apache.xmlgraphics.ps.PSGenerator; + +/** + * Represents the %%Pages DSC comment. + */ +public class DSCCommentPages extends AbstractDSCComment { + + private int pageCount = -1; + + /** + * Creates a new instance. + */ + public DSCCommentPages() { + } + + /** + * Creates a new instance. + * @param pageCount the number of pages + */ + public DSCCommentPages(int pageCount) { + this.pageCount = pageCount; + } + + /** + * Returns the page count. + * @return the page count + */ + public int getPageCount() { + return this.pageCount; + } + + /** + * Sets the page count. + * @param count the new page count + */ + public void setPageCount(int count) { + this.pageCount = count; + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.DSCComment#getName() + */ + public String getName() { + return DSCConstants.PAGES; + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.DSCComment#hasValues() + */ + public boolean hasValues() { + return true; + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.DSCComment#parseValue(java.lang.String) + */ + public void parseValue(String value) { + this.pageCount = Integer.parseInt(value); + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.DSCEvent#generate(org.apache.xmlgraphics.ps.PSGenerator) + */ + public void generate(PSGenerator gen) throws IOException { + if (getPageCount() > 0) { + gen.writeDSCComment(getName(), getPageCount()); + } else { + gen.writeDSCComment(getName(), DSCConstants.ATEND); + } + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCCommentTitle.java b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCCommentTitle.java new file mode 100644 index 0000000..31a2b85 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCCommentTitle.java @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DSCCommentTitle.java 727407 2008-12-17 15:05:45Z jeremias $ */ + +package org.apache.xmlgraphics.ps.dsc.events; + +import java.io.IOException; +import java.util.Iterator; +import java.util.List; + +import org.apache.xmlgraphics.ps.DSCConstants; +import org.apache.xmlgraphics.ps.PSGenerator; + +/** + * Represents a %%Title DSC comment. + */ +public class DSCCommentTitle extends AbstractDSCComment { + + private String title; + + /** + * Creates a new instance. + */ + public DSCCommentTitle() { + } + + /** + * Creates a new instance. + * @param title the title text + */ + public DSCCommentTitle(String title) { + this.title = title; + } + + /** + * Returns the title. + * @return the title + */ + public String getTitle() { + return this.title; + } + + /** {@inheritDoc} */ + public String getName() { + return DSCConstants.TITLE; + } + + /** {@inheritDoc} */ + public boolean hasValues() { + return true; + } + + /** {@inheritDoc} */ + public void parseValue(String value) { + List params = splitParams(value); + Iterator iter = params.iterator(); + this.title = (String)iter.next(); + } + + /** {@inheritDoc} */ + public void generate(PSGenerator gen) throws IOException { + gen.writeDSCComment(getName(), getTitle()); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCEvent.java b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCEvent.java new file mode 100644 index 0000000..15f7ddb --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCEvent.java @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DSCEvent.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.ps.dsc.events; + +import java.io.IOException; + +import org.apache.xmlgraphics.ps.PSGenerator; +import org.apache.xmlgraphics.ps.dsc.DSCParserConstants; + +/** + * Interface representing a DSC event. A DSC event can be a DSC comment, a PostScript comment + * or a line of PostScript code. + */ +public interface DSCEvent extends DSCParserConstants { + + /** + * Returns the event type. + * @return the event type (see {@link DSCParserConstants}) + */ + int getEventType(); + + /** + * Casts this instance to a DSCComment if possible. + * @return this event as a DSCComment + * @throws ClassCastException if the event is no DSCComment + */ + DSCComment asDSCComment(); + + /** + * Casts this instance to a PostScriptLine if possible. + * @return this event as a PostScriptLine + * @throws ClassCastException if the event is no PostScriptLine + */ + PostScriptLine asLine(); + + /** + * Indicates whether the instance is a DSC comment. + * @return true if the instance is a DSC comment + */ + boolean isDSCComment(); + + /** + * Indicates whether the instance is a PostScript comment. + * @return true if the instance is a PostScript comment + */ + boolean isComment(); + + /** + * Indicates whether the instance is a header comment. + * @return true if the instance is a header comment + */ + boolean isHeaderComment(); + + /** + * Indicates whether the instance is a PostScript line. + * @return true if the instance is a PostScript line + */ + boolean isLine(); + + /** + * Writes the event to the given PSGenerator. + * @param gen the PSGenerator to write to + * @throws IOException In case of an I/O error + */ + void generate(PSGenerator gen) throws IOException; + +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCHeaderComment.java b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCHeaderComment.java new file mode 100644 index 0000000..f2158b2 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/DSCHeaderComment.java @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DSCHeaderComment.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.ps.dsc.events; + +import java.io.IOException; + +import org.apache.xmlgraphics.ps.DSCConstants; +import org.apache.xmlgraphics.ps.PSGenerator; + +/** + * Represents a DSC header comment (beginning with "%!). + */ +public class DSCHeaderComment extends AbstractEvent { + + private String comment; + + /** + * Creates a new instance. + * @param comment the comment + */ + public DSCHeaderComment(String comment) { + this.comment = comment; + } + + /** + * Returns the comment. + * @return the comment + */ + public String getComment() { + return this.comment; + } + + /** + * Indicates whether the file started by this comments is DSC 3.0 compliant. + * @return true if the file is DSC 3.0 compliant. + */ + public boolean isPSAdobe30() { + return getComment().startsWith(DSCConstants.PS_ADOBE_30.substring(2)); + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.DSCEvent#generate( + * org.apache.xmlgraphics.ps.PSGenerator) + */ + public void generate(PSGenerator gen) throws IOException { + gen.writeln("%!" + getComment()); + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.DSCEvent#getEventType() + */ + public int getEventType() { + return HEADER_COMMENT; + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.AbstractEvent#isHeaderComment() + */ + public boolean isHeaderComment() { + return true; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/dsc/events/PostScriptComment.java b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/PostScriptComment.java new file mode 100644 index 0000000..5cc4a09 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/PostScriptComment.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PostScriptComment.java 1681108 2015-05-22 13:26:12Z ssteiner $ */ + +package org.apache.xmlgraphics.ps.dsc.events; + +import java.io.IOException; + +import org.apache.xmlgraphics.ps.PSGenerator; + +/** + * Represents a PostScript comment + */ +public class PostScriptComment extends AbstractEvent { + + private String comment; + + /** + * Creates a new instance. + * @param comment the comment + */ + public PostScriptComment(String comment) { + if (comment != null && comment.startsWith("%")) { + this.comment = comment.substring(1); + } else { + this.comment = comment; + } + } + + /** + * Returns the comment text. + * @return the comment (without the "%" prefix) + */ + public String getComment() { + return this.comment; + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.DSCEvent#generate(org.apache.xmlgraphics.ps.PSGenerator) + */ + public void generate(PSGenerator gen) throws IOException { + gen.commentln("%" + getComment()); + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.DSCEvent#getEventType() + */ + public int getEventType() { + return COMMENT; + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.AbstractEvent#isComment() + */ + public boolean isComment() { + return true; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/dsc/events/PostScriptLine.java b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/PostScriptLine.java new file mode 100644 index 0000000..067ae59 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/PostScriptLine.java @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PostScriptLine.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.ps.dsc.events; + +import java.io.IOException; + +import org.apache.xmlgraphics.ps.PSGenerator; + +/** + * Represents a line of PostScript code. + */ +public class PostScriptLine extends AbstractEvent { + + private String line; + + /** + * Creates a new instance. + * @param line the code line + */ + public PostScriptLine(String line) { + this.line = line; + } + + /** + * Returns the code line. + * @return the code line + */ + public String getLine() { + return this.line; + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.DSCEvent#generate(org.apache.xmlgraphics.ps.PSGenerator) + */ + public void generate(PSGenerator gen) throws IOException { + gen.writeln(getLine()); + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.DSCEvent#getEventType() + */ + public int getEventType() { + return LINE; + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.AbstractEvent#asLine() + */ + public PostScriptLine asLine() { + return this; + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.AbstractEvent#isLine() + */ + public boolean isLine() { + return true; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/dsc/events/UnparsedDSCComment.java b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/UnparsedDSCComment.java new file mode 100644 index 0000000..dc1e2ba --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/UnparsedDSCComment.java @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: UnparsedDSCComment.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.ps.dsc.events; + +import java.io.IOException; + +import org.apache.xmlgraphics.ps.PSGenerator; + +/** + * Represents a DSC comment that is not parsed into one of the concrete DSCComment subclasses. + * It is used whenever a DSC comment is encountered that is unknown to the parser. + * @see org.apache.xmlgraphics.ps.dsc.DSCCommentFactory + */ +public class UnparsedDSCComment extends AbstractEvent implements DSCComment { + + private String name; + private String value; + + /** + * Creates a new instance. + * @param name the name of the DSC comment + */ + public UnparsedDSCComment(String name) { + this.name = name; + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.DSCComment#getName() + */ + public String getName() { + return this.name; + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.DSCComment#hasValues() + */ + public boolean hasValues() { + return value != null; + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.DSCComment#isAtend() + */ + public boolean isAtend() { + return false; + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.DSCComment#parseValue(java.lang.String) + */ + public void parseValue(String value) { + this.value = value; + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.DSCEvent#generate(org.apache.xmlgraphics.ps.PSGenerator) + */ + public void generate(PSGenerator gen) throws IOException { + gen.writeln("%%" + name + (hasValues() ? ": " + value : "")); + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.AbstractEvent#isDSCComment() + */ + public boolean isDSCComment() { + return true; + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.DSCEvent#getEventType() + */ + public int getEventType() { + return DSC_COMMENT; + } + + /** + * @see org.apache.xmlgraphics.ps.dsc.events.AbstractEvent#asDSCComment() + */ + public DSCComment asDSCComment() { + return this; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/dsc/events/package.html b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/package.html new file mode 100644 index 0000000..edc30f8 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/dsc/events/package.html @@ -0,0 +1,23 @@ + + + +org.apache.xmlgraphics.ps.dsc.events Package + +

    Event classes used by the DSC parser.

    + + \ No newline at end of file diff --git a/src/main/java/org/apache/xmlgraphics/ps/dsc/package.html b/src/main/java/org/apache/xmlgraphics/ps/dsc/package.html new file mode 100644 index 0000000..7953747 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/dsc/package.html @@ -0,0 +1,23 @@ + + + +org.apache.xmlgraphics.ps.dsc Package + +

    Tools for DSC-compliant PostScript files (DSC = Document Structuring Conventions).

    + + \ No newline at end of file diff --git a/src/main/java/org/apache/xmlgraphics/ps/dsc/tools/DSCTools.java b/src/main/java/org/apache/xmlgraphics/ps/dsc/tools/DSCTools.java new file mode 100644 index 0000000..dbd2d74 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/dsc/tools/DSCTools.java @@ -0,0 +1,124 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DSCTools.java 1681108 2015-05-22 13:26:12Z ssteiner $ */ + +package org.apache.xmlgraphics.ps.dsc.tools; + +import java.io.IOException; + +import org.apache.xmlgraphics.ps.DSCConstants; +import org.apache.xmlgraphics.ps.PSGenerator; +import org.apache.xmlgraphics.ps.dsc.DSCException; +import org.apache.xmlgraphics.ps.dsc.DSCParser; +import org.apache.xmlgraphics.ps.dsc.DSCParserConstants; +import org.apache.xmlgraphics.ps.dsc.events.DSCComment; +import org.apache.xmlgraphics.ps.dsc.events.DSCEvent; +import org.apache.xmlgraphics.ps.dsc.events.DSCHeaderComment; +import org.apache.xmlgraphics.ps.dsc.events.PostScriptComment; + +/** + * Helper methods commonly used when dealing with DSC-compliant PostScript files. + */ +public final class DSCTools implements DSCParserConstants { + + private DSCTools() { + } + + /** + * Indicates whether the given event ends a header comment section according to the rules in + * DSC 3.0, chapter 4.4. + * @param event the event to check + * @return true if a header comment section would be ended either explicitely or implicitely + * by the given event + */ + public static boolean headerCommentsEndHere(DSCEvent event) { + switch (event.getEventType()) { + case DSC_COMMENT: + DSCComment comment = event.asDSCComment(); + return (comment.getName().equals(DSCConstants.END_COMMENTS)); + case COMMENT: + assert event instanceof PostScriptComment; + String s = ((PostScriptComment)event).getComment(); + if (s == null || s.length() == 0) { + return true; + } else { + char c = s.charAt(0); + return ("\n\t ".indexOf(c) >= 0); + } + default: + return true; + } + } + + /** + * Verifies that the file being parsed is a DSC 3.0 file. + * @param parser the DSC parser + * @return the header comment event + * @throws DSCException In case of a violation of the DSC spec + * @throws IOException In case of an I/O problem + */ + public static DSCHeaderComment checkAndSkipDSC30Header(DSCParser parser) + throws DSCException, IOException { + if (!parser.hasNext()) { + throw new DSCException("File has no content"); + } + DSCEvent event = parser.nextEvent(); + if (event.getEventType() == HEADER_COMMENT) { + DSCHeaderComment header = (DSCHeaderComment)event; + if (!header.isPSAdobe30()) { + throw new DSCException("PostScript file does not start with '" + + DSCConstants.PS_ADOBE_30 + "'"); + } + return header; + } else { + throw new DSCException("PostScript file does not start with '" + + DSCConstants.PS_ADOBE_30 + "'"); + } + } + + /** + * Advances the parser to the next page or to the trailer or the end of file comment. + * @param parser the DSC parser + * @param gen the PSGenerator instance to pass the skipped events through to + * @return the DSC comment found (Page, Trailer or EOF) + * @throws IOException In case of an I/O error + * @throws DSCException In case of a violation of the DSC spec + */ + public static DSCComment nextPageOrTrailer(DSCParser parser, PSGenerator gen) + throws IOException, DSCException { + while (parser.hasNext()) { + DSCEvent event = parser.nextEvent(); + if (event.getEventType() == DSC_COMMENT) { + DSCComment comment = event.asDSCComment(); + if (DSCConstants.PAGE.equals(comment.getName())) { + return comment; + } else if (DSCConstants.TRAILER.equals(comment.getName())) { + return comment; + } + } else if (event.getEventType() == EOF) { + //The Trailer may be missing + return event.asDSCComment(); + } + if (gen != null) { + event.generate(gen); //Pipe through to PSGenerator + } + } + return null; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/dsc/tools/PageExtractor.java b/src/main/java/org/apache/xmlgraphics/ps/dsc/tools/PageExtractor.java new file mode 100644 index 0000000..c1288ac --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/dsc/tools/PageExtractor.java @@ -0,0 +1,122 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PageExtractor.java 1345683 2012-06-03 14:50:33Z gadams $ */ + +package org.apache.xmlgraphics.ps.dsc.tools; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.apache.xmlgraphics.ps.DSCConstants; +import org.apache.xmlgraphics.ps.PSGenerator; +import org.apache.xmlgraphics.ps.dsc.DSCException; +import org.apache.xmlgraphics.ps.dsc.DSCFilter; +import org.apache.xmlgraphics.ps.dsc.DSCParser; +import org.apache.xmlgraphics.ps.dsc.DSCParserConstants; +import org.apache.xmlgraphics.ps.dsc.DefaultNestedDocumentHandler; +import org.apache.xmlgraphics.ps.dsc.events.DSCComment; +import org.apache.xmlgraphics.ps.dsc.events.DSCCommentPage; +import org.apache.xmlgraphics.ps.dsc.events.DSCCommentPages; +import org.apache.xmlgraphics.ps.dsc.events.DSCEvent; +import org.apache.xmlgraphics.ps.dsc.events.DSCHeaderComment; + +/** + * This class can extract a certain range of pages from a DSC-compliant PostScript file. + */ +public final class PageExtractor implements DSCParserConstants { + + private PageExtractor() { + } + + /** + * Parses a DSC-compliant file and pipes the content through to the OutputStream omitting + * all pages not within the range. + * @param in the InputStream to parse from + * @param out the OutputStream to write the modified file to + * @param from the starting page (1-based) + * @param to the last page (inclusive, 1-based) + * @throws IOException In case of an I/O error + * @throws DSCException In case of a violation of the DSC spec + */ + public static void extractPages(InputStream in, OutputStream out, int from, int to) + throws IOException, DSCException { + if (from <= 0) { + throw new IllegalArgumentException("'from' page number must be 1 or higher"); + } + if (to < from) { + throw new IllegalArgumentException( + "'to' page number must be equal or larger than the 'from' page number"); + } + + DSCParser parser = new DSCParser(in); + PSGenerator gen = new PSGenerator(out); + parser.addListener(new DefaultNestedDocumentHandler(gen)); + int pageCount = 0; + + //Skip DSC header + DSCHeaderComment header = DSCTools.checkAndSkipDSC30Header(parser); + header.generate(gen); + //Set number of pages + DSCCommentPages pages = new DSCCommentPages(to - from + 1); + pages.generate(gen); + + parser.setFilter(new DSCFilter() { + public boolean accept(DSCEvent event) { + if (event.isDSCComment()) { + //Filter %%Pages which we add manually above + return !event.asDSCComment().getName().equals(DSCConstants.PAGES); + } else { + return true; + } + } + }); + + //Skip the prolog and to the first page + DSCComment pageOrTrailer = parser.nextDSCComment(DSCConstants.PAGE, gen); + if (pageOrTrailer == null) { + throw new DSCException("Page expected, but none found"); + } + parser.setFilter(null); //Remove filter + + //Process individual pages (and skip as necessary) + while (true) { + DSCCommentPage page = (DSCCommentPage)pageOrTrailer; + boolean validPage = (page.getPagePosition() >= from && page.getPagePosition() <= to); + if (validPage) { + page.setPagePosition(page.getPagePosition() - from + 1); + page.generate(gen); + pageCount++; + } + pageOrTrailer = DSCTools.nextPageOrTrailer(parser, (validPage ? gen : null)); + if (pageOrTrailer == null) { + throw new DSCException("File is not DSC-compliant: Unexpected end of file"); + } else if (!DSCConstants.PAGE.equals(pageOrTrailer.getName())) { + pageOrTrailer.generate(gen); + break; + } + } + + //Write the rest + while (parser.hasNext()) { + DSCEvent event = parser.nextEvent(); + event.generate(gen); + } + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/ps/dsc/tools/package.html b/src/main/java/org/apache/xmlgraphics/ps/dsc/tools/package.html new file mode 100644 index 0000000..cc69d06 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/dsc/tools/package.html @@ -0,0 +1,23 @@ + + + +org.apache.xmlgraphics.ps.dsc.tools Package + +

    Tools for working with DSC-compliant PostScript files.

    + + \ No newline at end of file diff --git a/src/main/java/org/apache/xmlgraphics/ps/package.html b/src/main/java/org/apache/xmlgraphics/ps/package.html new file mode 100644 index 0000000..47680d1 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/ps/package.html @@ -0,0 +1,23 @@ + + + +org.apache.xmlgraphics.commons.ps Package + +

    Classes for the creation of PostScript files.

    + + \ No newline at end of file diff --git a/src/main/java/org/apache/xmlgraphics/util/ClasspathResource.java b/src/main/java/org/apache/xmlgraphics/util/ClasspathResource.java new file mode 100644 index 0000000..3776918 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/util/ClasspathResource.java @@ -0,0 +1,176 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ClasspathResource.java 1804124 2017-08-04 14:13:54Z ssteiner $ */ + +package org.apache.xmlgraphics.util; + +import java.io.IOException; +import java.net.URL; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Vector; +import java.util.jar.Attributes; +import java.util.jar.Manifest; + +/** + * A class to find resources in the classpath by their mime-type specified in + * the MANIFEST. + *

    + * This class searches for content entries in all META-INF/MANIFEST.MF files. It + * will find files with a given Content-Type: attribute. This allows to add + * arbitrary resources by content-type just by creating a JAR wrapper and adding + * them to the classpath. + *

    + * Example:
    + * + *

    + * Name: test.txt
    + * Content-Type: text/plain
    + * 
    + */ +public final class ClasspathResource { + + /** + * Actual Type: Map<String,List<URL>>. + */ + private final Map contentMappings; + + private static final String MANIFEST_PATH = "META-INF/MANIFEST.MF"; + + private static final String CONTENT_TYPE_KEY = "Content-Type"; + + private static ClasspathResource classpathResource; + + private ClasspathResource() { + contentMappings = new HashMap(); + loadManifests(); + } + + /** + * Retrieve the singleton instance of this class. + * + * @return the ClassPathResource instance. + */ + public static synchronized ClasspathResource getInstance() { + if (classpathResource == null) { + classpathResource = new ClasspathResource(); + } + return classpathResource; + } + + /* Actual return type: Set */ + private Set getClassLoadersForResources() { + Set v = new HashSet(); + try { + ClassLoader l = ClassLoader.getSystemClassLoader(); + if (l != null) { + v.add(l); + } + } catch (SecurityException e) { + // Ignore + } + try { + ClassLoader l = Thread.currentThread().getContextClassLoader(); + if (l != null) { + v.add(l); + } + } catch (SecurityException e) { + // Ignore + } + try { + ClassLoader l = ClasspathResource.class.getClassLoader(); + if (l != null) { + v.add(l); + } + } catch (SecurityException e) { + // Ignore + } + return v; + } + + private void loadManifests() { + Enumeration e; + try { + + for (Object o1 : getClassLoadersForResources()) { + ClassLoader classLoader = (ClassLoader) o1; + + e = classLoader.getResources(MANIFEST_PATH); + + while (e.hasMoreElements()) { + final URL u = (URL) e.nextElement(); + try { + final Manifest manifest = new Manifest(u.openStream()); + final Map entries = manifest.getEntries(); + for (Object o : entries.entrySet()) { + final Map.Entry entry = (Map.Entry) o; + final String name = (String) entry.getKey(); + final Attributes attributes = (Attributes) entry + .getValue(); + final String contentType = attributes + .getValue(CONTENT_TYPE_KEY); + if (contentType != null) { + addToMapping(contentType, name, classLoader); + } + } + } catch (IOException io) { + // TODO: Log. + } + } + } + + } catch (IOException io) { + // TODO: Log. + } + } + + private void addToMapping(final String contentType, final String name, + final ClassLoader classLoader) { + List existingFiles = (List) contentMappings.get(contentType); + if (existingFiles == null) { + existingFiles = new Vector(); + contentMappings.put(contentType, existingFiles); + } + final URL url = classLoader.getResource(name); + if (url != null) { + existingFiles.add(url); + } + } + + /** + * Retrieve a list of resources known to have the given mime-type. + * + * @param mimeType + * the mime-type to search for. + * @return a List<URL>, guaranteed to be != null. + */ + public List listResourcesOfMimeType(final String mimeType) { + final List content = (List) contentMappings.get(mimeType); + if (content == null) { + return Collections.EMPTY_LIST; + } else { + return content; + } + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/util/DateFormatUtil.java b/src/main/java/org/apache/xmlgraphics/util/DateFormatUtil.java new file mode 100644 index 0000000..caa04bd --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/util/DateFormatUtil.java @@ -0,0 +1,158 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.xmlgraphics.util; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +public final class DateFormatUtil { + + private static final String ISO_8601_DATE_PATTERN = "yyyy-MM-dd'T'HH:mm:ss"; + + private DateFormatUtil() { + } + + /** + * Formats the date according to PDF format. See section 3.8.2 of the PDF 1.4 specification. + * @param date The date time to format + * @param timeZone The time zone used to format the date + * @return a formatted date according to PDF format (based on ISO 8824) + */ + public static String formatPDFDate(Date date, TimeZone timeZone) { + DateFormat dateFormat = createDateFormat("'D:'yyyyMMddHHmmss", timeZone); + return formatDate(date, dateFormat, '\'', true); + } + + /** + * Formats the date according to ISO 8601 standard. + * @param date The date time to format + * @param timeZone The time zone used to format the date + * @return a formatted date according to ISO 8601 + */ + public static String formatISO8601(Date date, TimeZone timeZone) { + DateFormat dateFormat = createDateFormat(ISO_8601_DATE_PATTERN, timeZone); + return formatDate(date, dateFormat, ':', false); + } + + private static DateFormat createDateFormat(String format, TimeZone timeZone) { + DateFormat dateFormat = new SimpleDateFormat(format, Locale.ENGLISH); + dateFormat.setTimeZone(timeZone); + return dateFormat; + } + + /** + * Formats the date according to the specified format and returns as a string. + * @param date The date / time object to format + * @param dateFormat The date format to use when outputting the date + * @param delimiter The character used to separate the time zone difference hours and minutes + * @param endWithDelimiter Determines whether the date string will end with the delimiter character + * @return the formatted date string + */ + private static String formatDate(Date date, DateFormat dateFormat, char delimiter, + boolean endWithDelimiter) { + Calendar cal = Calendar.getInstance(dateFormat.getTimeZone(), Locale.ENGLISH); + cal.setTime(date); + int offset = getOffsetInMinutes(cal); + StringBuilder sb = new StringBuilder(dateFormat.format(date)); + appendOffset(sb, delimiter, offset, endWithDelimiter); + return sb.toString(); + } + + private static int getOffsetInMinutes(Calendar cal) { + int offset = cal.get(Calendar.ZONE_OFFSET); + offset += cal.get(Calendar.DST_OFFSET); + offset /= (1000 * 60); + return offset; + } + + private static void appendOffset(StringBuilder sb, char delimiter, int offset, boolean endWithDelimiter) { + if (offset == 0) { + appendOffsetUTC(sb); + } else { + appendOffsetNoUTC(sb, delimiter, offset, endWithDelimiter); + } + } + + private static void appendOffsetUTC(StringBuilder sb) { + sb.append('Z'); + } + + private static void appendOffsetNoUTC(StringBuilder sb, char delimiter, int offset, + boolean endWithDelimiter) { + int zoneOffsetHours = offset / 60; + appendOffsetSign(sb, zoneOffsetHours); + appendPaddedNumber(sb, Math.abs(zoneOffsetHours)); + sb.append(delimiter); + appendPaddedNumber(sb, Math.abs(offset % 60)); + if (endWithDelimiter) { + sb.append(delimiter); + } + } + + private static void appendOffsetSign(StringBuilder sb, int zoneOffsetHours) { + if (zoneOffsetHours >= 0) { + sb.append('+'); + } else { + sb.append('-'); + } + } + + private static void appendPaddedNumber(StringBuilder sb, int number) { + if (number < 10) { + sb.append('0'); + } + sb.append(number); + } + + /** + * Parses an ISO 8601 date and time value. + * @param date the date and time value as an ISO 8601 string + * @return the parsed date/time + */ + public static Date parseISO8601Date(String date) { + final String errorMessage = "Invalid ISO 8601 date format: "; + date = formatDateToParse(date, errorMessage); + DateFormat dateFormat = new SimpleDateFormat(ISO_8601_DATE_PATTERN + "Z"); + try { + return dateFormat.parse(date); + } catch (ParseException ex) { + throw new IllegalArgumentException(errorMessage + date); + } + } + + private static String formatDateToParse(String date, String errorMessage) { + /* Remove the colon from the time zone difference (+08:00) so that it can be parsed + * by the SimpleDateFormat string. + */ + if (!date.contains("Z")) { + int lastColonIndex = date.lastIndexOf(":"); + if (lastColonIndex < 0) { + throw new IllegalArgumentException(errorMessage + date); + } + date = date.substring(0, lastColonIndex) + date.substring(lastColonIndex + 1, date.length()); + } else { + date = date.replace("Z", "+0000"); + } + return date; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/util/DoubleFormatUtil.java b/src/main/java/org/apache/xmlgraphics/util/DoubleFormatUtil.java new file mode 100644 index 0000000..755b718 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/util/DoubleFormatUtil.java @@ -0,0 +1,381 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DoubleFormatUtil.java 1732019 2016-02-24 05:01:10Z gadams $ */ + +package org.apache.xmlgraphics.util; + +/** + * This class implements fast, thread-safe format of a double value + * with a given number of decimal digits. + *

    + * The contract for the format methods is this one: + * if the source is greater than or equal to 1 (in absolute value), + * use the decimals parameter to define the number of decimal digits; else, + * use the precision parameter to define the number of decimal digits. + *

    + * A few examples (consider decimals being 4 and precision being 8): + *

      + *
    • 0.0 should be rendered as "0" + *
    • 0.1 should be rendered as "0.1" + *
    • 1234.1 should be rendered as "1234.1" + *
    • 1234.1234567 should be rendered as "1234.1235" (note the trailing 5! Rounding!) + *
    • 1234.00001 should be rendered as "1234" + *
    • 0.00001 should be rendered as "0.00001" (here you see the effect of the "precision" parameter) + *
    • 0.00000001 should be rendered as "0.00000001" + *
    • 0.000000001 should be rendered as "0" + *
    + * + * Originally authored by Julien Aymé. + */ +public final class DoubleFormatUtil { + + private DoubleFormatUtil() { + } + + /** + * Rounds the given source value at the given precision + * and writes the rounded value into the given target + * + * @param source the source value to round + * @param decimals the decimals to round at (use if abs(source) ≥ 1.0) + * @param precision the precision to round at (use if abs(source) < 1.0) + * @param target the buffer to write to + */ + public static void formatDouble(double source, int decimals, int precision, StringBuffer target) { + int scale = (Math.abs(source) >= 1.0) ? decimals : precision; + if (tooManyDigitsUsed(source, scale) || tooCloseToRound(source, scale)) { + formatDoublePrecise(source, decimals, precision, target); + } else { + formatDoubleFast(source, decimals, precision, target); + } + } + + /** + * Rounds the given source value at the given precision + * and writes the rounded value into the given target + *

    + * This method internally uses the String representation of the source value, + * in order to avoid any double precision computation error. + * + * @param source the source value to round + * @param decimals the decimals to round at (use if abs(source) ≥ 1.0) + * @param precision the precision to round at (use if abs(source) < 1.0) + * @param target the buffer to write to + */ + public static void formatDoublePrecise(double source, int decimals, int precision, StringBuffer target) { + if (isRoundedToZero(source, decimals, precision)) { + // Will always be rounded to 0 + target.append('0'); + return; + } else if (Double.isNaN(source) || Double.isInfinite(source)) { + // Cannot be formated + target.append(Double.toString(source)); + return; + } + + boolean negative = source < 0.0; + if (negative) { + source = -source; + // Done once and for all + target.append('-'); + } + int scale = (source >= 1.0) ? decimals : precision; + + // The only way to format precisely the double is to use the String + // representation of the double, and then to do mathematical integer operation on it. + String s = Double.toString(source); + if (source >= 1e-3 && source < 1e7) { + // Plain representation of double: "intPart.decimalPart" + int dot = s.indexOf('.'); + String decS = s.substring(dot + 1); + int decLength = decS.length(); + if (scale >= decLength) { + if ("0".equals(decS)) { + // source is a mathematical integer + target.append(s.substring(0, dot)); + } else { + target.append(s); + // Remove trailing zeroes + for (int l = target.length() - 1; l >= 0 && target.charAt(l) == '0'; l--) { + target.setLength(l); + } + } + return; + } else if (scale + 1 < decLength) { + // ignore unnecessary digits + decLength = scale + 1; + decS = decS.substring(0, decLength); + } + long intP = Long.parseLong(s.substring(0, dot)); + long decP = Long.parseLong(decS); + format(target, scale, intP, decP); + } else { + // Scientific representation of double: "x.xxxxxEyyy" + int dot = s.indexOf('.'); + assert dot >= 0; + int exp = s.indexOf('E'); + assert exp >= 0; + int exposant = Integer.parseInt(s.substring(exp + 1)); + String intS = s.substring(0, dot); + String decS = s.substring(dot + 1, exp); + int decLength = decS.length(); + if (exposant >= 0) { + int digits = decLength - exposant; + if (digits <= 0) { + // no decimal part, + // no rounding involved + target.append(intS); + target.append(decS); + for (int i = -digits; i > 0; i--) { + target.append('0'); + } + } else if (digits <= scale) { + // decimal part precision is lower than scale, + // no rounding involved + target.append(intS); + target.append(decS.substring(0, exposant)); + target.append('.'); + target.append(decS.substring(exposant)); + } else { + // decimalDigits > scale, + // Rounding involved + long intP = Long.parseLong(intS) * tenPow(exposant) + Long.parseLong(decS.substring(0, exposant)); + long decP = Long.parseLong(decS.substring(exposant, exposant + scale + 1)); + format(target, scale, intP, decP); + } + } else { + // Only a decimal part is supplied + exposant = -exposant; + int digits = scale - exposant + 1; + if (digits < 0) { + target.append('0'); + } else if (digits == 0) { + long decP = Long.parseLong(intS); + format(target, scale, 0L, decP); + } else if (decLength < digits) { + long decP = Long.parseLong(intS) * tenPow(decLength + 1) + Long.parseLong(decS) * 10; + format(target, exposant + decLength, 0L, decP); + } else { + long subDecP = Long.parseLong(decS.substring(0, digits)); + long decP = Long.parseLong(intS) * tenPow(digits) + subDecP; + format(target, scale, 0L, decP); + } + } + } + } + + /** + * Returns true if the given source value will be rounded to zero + * + * @param source the source value to round + * @param decimals the decimals to round at (use if abs(source) ≥ 1.0) + * @param precision the precision to round at (use if abs(source) < 1.0) + * @return true if the source value will be rounded to zero + */ + private static boolean isRoundedToZero(double source, int decimals, int precision) { + // Use 4.999999999999999 instead of 5 since in some cases, 5.0 / 1eN > 5e-N (e.g. for N = 37, 42, 45, 66, ...) + return source == 0.0 || Math.abs(source) < 4.999999999999999 / tenPowDouble(Math.max(decimals, precision) + 1); + } + + /** + * Most used power of ten (to avoid the cost of Math.pow(10, n) + */ + private static final long[] POWERS_OF_TEN_LONG = new long[19]; + private static final double[] POWERS_OF_TEN_DOUBLE = new double[30]; + static { + POWERS_OF_TEN_LONG[0] = 1L; + for (int i = 1; i < POWERS_OF_TEN_LONG.length; i++) { + POWERS_OF_TEN_LONG[i] = POWERS_OF_TEN_LONG[i - 1] * 10L; + } + for (int i = 0; i < POWERS_OF_TEN_DOUBLE.length; i++) { + POWERS_OF_TEN_DOUBLE[i] = Double.parseDouble("1e" + i); + } + } + + /** + * Returns ten to the power of n + * + * @param n the nth power of ten to get + * @return ten to the power of n + */ + public static long tenPow(int n) { + assert n >= 0; + return n < POWERS_OF_TEN_LONG.length ? POWERS_OF_TEN_LONG[n] : (long) Math.pow(10, n); + } + + private static double tenPowDouble(int n) { + assert n >= 0; + return n < POWERS_OF_TEN_DOUBLE.length ? POWERS_OF_TEN_DOUBLE[n] : Math.pow(10, n); + } + + /** + * Helper method to do the custom rounding used within formatDoublePrecise + * + * @param target the buffer to write to + * @param scale the expected rounding scale + * @param intP the source integer part + * @param decP the source decimal part, truncated to scale + 1 digit + */ + private static void format(StringBuffer target, int scale, long intP, long decP) { + if (decP != 0L) { + // decP is the decimal part of source, truncated to scale + 1 digit. + // Custom rounding: add 5 + decP += 5L; + decP /= 10L; + if (decP >= tenPowDouble(scale)) { + intP++; + decP -= tenPow(scale); + } + if (decP != 0L) { + // Remove trailing zeroes + while (decP % 10L == 0L) { + decP = decP / 10L; + scale--; + } + } + } + target.append(intP); + if (decP != 0L) { + target.append('.'); + // Use tenPow instead of tenPowDouble for scale below 18, + // since the casting of decP to double may cause some imprecisions: + // E.g. for decP = 9999999999999999L and scale = 17, + // decP < tenPow(16) while (double) decP == tenPowDouble(16) + while (scale > 0 && (scale > 18 ? decP < tenPowDouble(--scale) : decP < tenPow(--scale))) { + // Insert leading zeroes + target.append('0'); + } + target.append(decP); + } + } + + /** + * Rounds the given source value at the given precision + * and writes the rounded value into the given target + *

    + * This method internally uses double precision computation and rounding, + * so the result may not be accurate (see formatDouble method for conditions). + * + * @param source the source value to round + * @param decimals the decimals to round at (use if abs(source) ≥ 1.0) + * @param precision the precision to round at (use if abs(source) < 1.0) + * @param target the buffer to write to + */ + public static void formatDoubleFast(double source, int decimals, int precision, StringBuffer target) { + if (isRoundedToZero(source, decimals, precision)) { + // Will always be rounded to 0 + target.append('0'); + return; + } else if (Double.isNaN(source) || Double.isInfinite(source)) { + // Cannot be formated + target.append(Double.toString(source)); + return; + } + + boolean isPositive = source >= 0.0; + source = Math.abs(source); + int scale = (source >= 1.0) ? decimals : precision; + + long intPart = (long) Math.floor(source); + double tenScale = tenPowDouble(scale); + double fracUnroundedPart = (source - intPart) * tenScale; + long fracPart = Math.round(fracUnroundedPart); + if (fracPart >= tenScale) { + intPart++; + fracPart = Math.round(fracPart - tenScale); + } + if (fracPart != 0L) { + // Remove trailing zeroes + while (fracPart % 10L == 0L) { + fracPart = fracPart / 10L; + scale--; + } + } + + if (intPart != 0L || fracPart != 0L) { + // non-zero value + if (!isPositive) { + // negative value, insert sign + target.append('-'); + } + // append integer part + target.append(intPart); + if (fracPart != 0L) { + // append fractional part + target.append('.'); + // insert leading zeroes + while (scale > 0 && fracPart < tenPowDouble(--scale)) { + target.append('0'); + } + target.append(fracPart); + } + } else { + target.append('0'); + } + } + + /** + * Returns the exponent of the given value + * + * @param value the value to get the exponent from + * @return the value's exponent + */ + public static int getExponant(double value) { + // See Double.doubleToRawLongBits javadoc or IEEE-754 spec + // to have this algorithm + long exp = Double.doubleToRawLongBits(value) & 0x7ff0000000000000L; + exp = exp >> 52; + return (int) (exp - 1023L); + } + + /** + * Returns true if the rounding is considered to use too many digits + * of the double for a fast rounding + * + * @param source the source to round + * @param scale the scale to round at + * @return true if the rounding will potentially use too many digits + */ + private static boolean tooManyDigitsUsed(double source, int scale) { + // if scale >= 308, 10^308 ~= Infinity + double decExp = Math.log10(source); + return scale >= 308 || decExp + scale >= 14.5; + } + + /** + * Returns true if the given source is considered to be too close + * of a rounding value for the given scale. + * + * @param source the source to round + * @param scale the scale to round at + * @return true if the source will be potentially rounded at the scale + */ + private static boolean tooCloseToRound(double source, int scale) { + source = Math.abs(source); + long intPart = (long) Math.floor(source); + double fracPart = (source - intPart) * tenPowDouble(scale); + double decExp = Math.log10(source); + double range = decExp + scale >= 12 ? .1 : .001; + double distanceToRound1 = Math.abs(fracPart - Math.floor(fracPart)); + double distanceToRound2 = Math.abs(fracPart - Math.floor(fracPart) - 0.5); + return distanceToRound1 <= range || distanceToRound2 <= range; + // .001 range: Totally arbitrary range, + // I never had a failure in 10e7 random tests with this value + // May be JVM dependent or architecture dependent + } +} diff --git a/src/main/java/org/apache/xmlgraphics/util/ImageIODebugUtil.java b/src/main/java/org/apache/xmlgraphics/util/ImageIODebugUtil.java new file mode 100644 index 0000000..a16f1d7 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/util/ImageIODebugUtil.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageIODebugUtil.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.util; + +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.metadata.IIOMetadataFormatImpl; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Result; +import javax.xml.transform.Source; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerConfigurationException; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + +import org.w3c.dom.Node; + +/** + * Helper class for debugging stuff in Image I/O. + * + * @version $Id: ImageIODebugUtil.java 1732018 2016-02-24 04:51:06Z gadams $ + */ +public final class ImageIODebugUtil { + + private ImageIODebugUtil() { + } + + public static void dumpMetadata(IIOMetadata meta, boolean nativeFormat) { + String format; + if (nativeFormat) { + format = meta.getNativeMetadataFormatName(); + } else { + format = IIOMetadataFormatImpl.standardMetadataFormatName; + } + Node node = meta.getAsTree(format); + dumpNode(node); + } + + public static void dumpNode(Node node) { + try { + TransformerFactory tf = TransformerFactory.newInstance(); + Transformer t = tf.newTransformer(); + t.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); + Source src = new DOMSource(node); + Result res = new StreamResult(System.out); + t.transform(src, res); + } catch (TransformerConfigurationException e) { + e.printStackTrace(); + } catch (TransformerException e) { + e.printStackTrace(); + } + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/util/MimeConstants.java b/src/main/java/org/apache/xmlgraphics/util/MimeConstants.java new file mode 100644 index 0000000..4b2ee06 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/util/MimeConstants.java @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: MimeConstants.java 1694449 2015-08-06 10:59:25Z ssteiner $ */ + +package org.apache.xmlgraphics.util; + +/** + * Frequently used MIME types for various file formats used by the XML Graphics project. + */ +public interface MimeConstants { + + /** Portable Document Format */ + String MIME_PDF = "application/pdf"; + + /** PostScript */ + String MIME_POSTSCRIPT = "application/postscript"; + /** Encapsulated PostScript (same MIME type as PostScript) */ + String MIME_EPS = MIME_POSTSCRIPT; + + /** HP's PCL */ + String MIME_PCL = "application/x-pcl"; + /** HP's PCL (alternative MIME type) */ + String MIME_PCL_ALT = "application/vnd.hp-PCL"; + + /** IBM's AFP */ + String MIME_AFP = "application/x-afp"; + + /** IBM's AFP (alternative MIME type) */ + String MIME_AFP_ALT = "application/vnd.ibm.modcap"; + + /** IBM's AFP IOCA subset for bilevel raster image */ + String MIME_AFP_IOCA_FS10 = "image/x-afp+fs10"; + + /** IBM's AFP IOCA subset for grayscale and color raster image */ + String MIME_AFP_IOCA_FS11 = "image/x-afp+fs11"; + + /** IBM's AFP IOCA subset for grayscale and color tiled raster image */ + String MIME_AFP_IOCA_FS45 = "image/x-afp+fs45"; + + /** IBM's AFP GOCA subset for graphical objects */ + String MIME_AFP_GOCA = "image/x-afp+goca"; + + String MIME_AFP_TRUETYPE = "image/x-afp+truetype"; + + /** Plain text */ + String MIME_PLAIN_TEXT = "text/plain"; + + /** Rich text format */ + String MIME_RTF = "application/rtf"; + /** Rich text format (alternative 1) */ + String MIME_RTF_ALT1 = "text/richtext"; + /** Rich text format (alternative 2) */ + String MIME_RTF_ALT2 = "text/rtf"; + + /** FrameMaker's MIF */ + String MIME_MIF = "application/mif"; + + /** Structured Vector Graphics */ + String MIME_SVG = "image/svg+xml"; + + /** GIF images */ + String MIME_GIF = "image/gif"; + /** PNG images */ + String MIME_PNG = "image/png"; + /** JPEG images */ + String MIME_JPEG = "image/jpeg"; + /** TIFF images */ + String MIME_TIFF = "image/tiff"; + + /** Proposed but non-registered MIME type for XSL-FO */ + String MIME_XSL_FO = "text/xsl"; + + /** Microsoft's Enhanced Metafile */ + String MIME_EMF = "image/x-emf"; + +} diff --git a/src/main/java/org/apache/xmlgraphics/util/QName.java b/src/main/java/org/apache/xmlgraphics/util/QName.java new file mode 100644 index 0000000..cc4afab --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/util/QName.java @@ -0,0 +1,138 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: QName.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.util; + +import java.io.Serializable; + +/** + * Represents a qualified name of an XML element or an XML attribute. + *

    + * Note: This class allows to carry a namespace prefix but it is not used in the equals() and + * hashCode() methods. + */ +public class QName implements Serializable { + + private static final long serialVersionUID = -5225376740044770690L; + + private String namespaceURI; + private String localName; + private String prefix; + private int hashCode; + + /** + * Main constructor. + * @param namespaceURI the namespace URI + * @param prefix the namespace prefix, may be null + * @param localName the local name + */ + public QName(String namespaceURI, String prefix, String localName) { + if (localName == null) { + throw new NullPointerException("Parameter localName must not be null"); + } + if (localName.length() == 0) { + throw new IllegalArgumentException("Parameter localName must not be empty"); + } + this.namespaceURI = namespaceURI; + this.prefix = prefix; + this.localName = localName; + this.hashCode = toHashString().hashCode(); + } + + /** + * Main constructor. + * @param namespaceURI the namespace URI + * @param qName the qualified name + */ + public QName(String namespaceURI, String qName) { + if (qName == null) { + throw new NullPointerException("Parameter localName must not be null"); + } + if (qName.length() == 0) { + throw new IllegalArgumentException("Parameter localName must not be empty"); + } + this.namespaceURI = namespaceURI; + int p = qName.indexOf(':'); + if (p > 0) { + this.prefix = qName.substring(0, p); + this.localName = qName.substring(p + 1); + } else { + this.prefix = null; + this.localName = qName; + } + this.hashCode = toHashString().hashCode(); + } + + /** @return the namespace URI */ + public String getNamespaceURI() { + return this.namespaceURI; + } + + /** @return the namespace prefix */ + public String getPrefix() { + return this.prefix; + } + + /** @return the local name */ + public String getLocalName() { + return this.localName; + } + + /** @return the fully qualified name */ + public String getQName() { + return getPrefix() != null ? getPrefix() + ':' + getLocalName() : getLocalName(); + } + + /** @see java.lang.Object#hashCode() */ + public int hashCode() { + return this.hashCode; + } + + /** @see java.lang.Object#equals(java.lang.Object) */ + public boolean equals(Object obj) { + if (obj == null) { + return false; + } else if (obj == this) { + return true; + } else { + if (obj instanceof QName) { + QName other = (QName)obj; + if ((getNamespaceURI() == null && other.getNamespaceURI() == null) + || getNamespaceURI().equals(other.getNamespaceURI())) { + return getLocalName().equals(other.getLocalName()); + } + } + } + return false; + } + + /** @see java.lang.Object#toString() */ + public String toString() { + return prefix != null + ? (prefix + ":" + localName) + : toHashString(); + } + + private String toHashString() { + return (namespaceURI != null + ? ("{" + namespaceURI + "}" + localName) + : localName); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/util/Service.java b/src/main/java/org/apache/xmlgraphics/util/Service.java new file mode 100644 index 0000000..7a08019 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/util/Service.java @@ -0,0 +1,213 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: Service.java 1780540 2017-01-27 11:10:50Z ssteiner $ */ + +package org.apache.xmlgraphics.util; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.net.URL; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.apache.commons.io.IOUtils; + +/** + * This class handles looking up service providers on the class path. + * It implements the system described in: + * + * JAR + * File Specification Under Service Provider. Note that this + * interface is very similar to the one they describe which seems to + * be missing in the JDK. + * + * @version $Id: Service.java 1780540 2017-01-27 11:10:50Z ssteiner $ + * + * Originally authored by Thomas DeWeese. + */ +public final class Service { + + private Service() { + } + + // Remember providers we have looked up before. + static Map> classMap = new java.util.HashMap>(); + static Map> instanceMap = new java.util.HashMap>(); + + /** + * Returns an iterator where each element should implement the + * interface (or subclass the baseclass) described by cls. The + * Classes are found by searching the classpath for service files + * named: 'META-INF/services/<fully qualified classname> that list + * fully qualifted classnames of classes that implement the + * service files classes interface. These classes must have + * default constructors. + * + * @param cls The class/interface to search for providers of. + */ + public static synchronized Iterator providers(Class cls) { + String serviceFile = getServiceFilename(cls); + + List l = instanceMap.get(serviceFile); + if (l != null) { + return l.iterator(); + } + + l = new java.util.ArrayList(); + instanceMap.put(serviceFile, l); + + ClassLoader cl = getClassLoader(cls); + if (cl != null) { + List names = getProviderNames(cls, cl); + for (String name : names) { + try { + // Try and load the class + Object obj = cl.loadClass(name).getDeclaredConstructor().newInstance(); + // stick it into our vector... + l.add(obj); + } catch (Exception ex) { + // Just try the next name + } + } + } + return l.iterator(); + } + + /** + * Returns an iterator where each element should be the name + * of a class that implements the + * interface (or subclass the baseclass) described by cls. The + * Classes are found by searching the classpath for service files + * named: 'META-INF/services/<fully qualified classname> that list + * fully qualified classnames of classes that implement the + * service files classes interface. + * + * @param cls The class/interface to search for providers of. + */ + public static synchronized Iterator providerNames(Class cls) { + String serviceFile = getServiceFilename(cls); + + List l = classMap.get(serviceFile); + if (l != null) { + return l.iterator(); + } + + l = new java.util.ArrayList(); + classMap.put(serviceFile, l); + l.addAll(getProviderNames(cls)); + return l.iterator(); + } + + /** + * Returns an iterator where each element should implement the + * interface (or subclass the baseclass) described by cls. The + * Classes are found by searching the classpath for service files + * named: 'META-INF/services/<fully qualified classname> that list + * fully qualified classnames of classes that implement the + * service files classes interface. These classes must have + * default constructors if returnInstances is true. + * + * This is a deprecated, type-unsafe legacy method. + * + * @param cls The class/interface to search for providers of. + * @param returnInstances true if the iterator should return instances rather than class names. + * @deprecated use the type-safe methods providers(Class) or providerNames(Class) instead. + */ + public static Iterator providers(Class cls, boolean returnInstances) { + return (returnInstances ? providers(cls) : providerNames(cls)); + } + + private static List getProviderNames(Class cls) { + return getProviderNames(cls, getClassLoader(cls)); + } + + private static List getProviderNames(Class cls, ClassLoader cl) { + List l = new java.util.ArrayList(); + + // No class loader so we can't find 'serviceFile'. + if (cl == null) { + return l; + } + + Enumeration e; + try { + e = cl.getResources(getServiceFilename(cls)); + } catch (IOException ioe) { + return l; + } + + while (e.hasMoreElements()) { + try { + URL u = e.nextElement(); + + InputStream is = u.openStream(); + Reader r = new InputStreamReader(is, "UTF-8"); + BufferedReader br = new BufferedReader(r); + try { + for (String line = br.readLine(); line != null; line = br.readLine()) { + // First strip any comment... + int idx = line.indexOf('#'); + if (idx != -1) { + line = line.substring(0, idx); + } + + // Trim whitespace. + line = line.trim(); + + if (line.length() != 0) { + l.add(line); + } + } + } finally { + IOUtils.closeQuietly(br); + IOUtils.closeQuietly(is); + } + } catch (Exception ex) { + // Just try the next file... + } + } + return l; + } + + private static ClassLoader getClassLoader(Class cls) { + ClassLoader cl = null; + try { + cl = cls.getClassLoader(); + } catch (SecurityException se) { + // Ooops! can't get his class loader. + } + // Can always request your own class loader. But it might be 'null'. + if (cl == null) { + cl = Service.class.getClassLoader(); + } + if (cl == null) { + cl = ClassLoader.getSystemClassLoader(); + } + return cl; + } + + private static String getServiceFilename(Class cls) { + return "META-INF/services/" + cls.getName(); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/util/UnitConv.java b/src/main/java/org/apache/xmlgraphics/util/UnitConv.java new file mode 100644 index 0000000..6074a17 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/util/UnitConv.java @@ -0,0 +1,217 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: UnitConv.java 1681108 2015-05-22 13:26:12Z ssteiner $ */ + +package org.apache.xmlgraphics.util; + +import java.awt.geom.AffineTransform; +import java.util.Locale; + +/** + * Utility class for unit conversions. + */ +public final class UnitConv { + + private UnitConv() { + } + + /** conversion factory from millimeters to inches. */ + public static final float IN2MM = 25.4f; + + /** conversion factory from centimeters to inches. */ + public static final float IN2CM = 2.54f; + + /** conversion factory from inches to points. */ + public static final int IN2PT = 72; + + /** Describes the unit pica. */ + public static final String PICA = "pc"; + + /** Describes the unit point. */ + public static final String POINT = "pt"; + + /** Describes the unit millimeter. */ + public static final String MM = "mm"; + + /** Describes the unit centimeter. */ + public static final String CM = "cm"; + + /** Describes the unit inch. */ + public static final String INCH = "in"; + + /** Describes the unit millipoint. */ + public static final String MPT = "mpt"; + + /** Describes the unit pixel. */ + public static final String PX = "px"; + + /** + * Converts millimeters (mm) to points (pt) + * @param mm the value in mm + * @return the value in pt + */ + public static double mm2pt(double mm) { + return mm * IN2PT / IN2MM; + } + + /** + * Converts millimeters (mm) to millipoints (mpt) + * @param mm the value in mm + * @return the value in mpt + */ + public static double mm2mpt(double mm) { + return mm * 1000 * IN2PT / IN2MM; + } + + /** + * Converts points (pt) to millimeters (mm) + * @param pt the value in pt + * @return the value in mm + */ + public static double pt2mm(double pt) { + return pt * IN2MM / IN2PT; + } + + /** + * Converts millimeters (mm) to inches (in) + * @param mm the value in mm + * @return the value in inches + */ + public static double mm2in(double mm) { + return mm / IN2MM; + } + + /** + * Converts inches (in) to millimeters (mm) + * @param in the value in inches + * @return the value in mm + */ + public static double in2mm(double in) { + return in * IN2MM; + } + + /** + * Converts inches (in) to millipoints (mpt) + * @param in the value in inches + * @return the value in mpt + */ + public static double in2mpt(double in) { + return in * IN2PT * 1000; + } + + /** + * Converts inches (in) to points (pt) + * @param in the value in inches + * @return the value in pt + */ + public static double in2pt(double in) { + return in * IN2PT; + } + + /** + * Converts millipoints (mpt) to inches (in) + * @param mpt the value in mpt + * @return the value in inches + */ + public static double mpt2in(double mpt) { + return mpt / IN2PT / 1000; + } + + /** + * Converts millimeters (mm) to pixels (px) + * @param mm the value in mm + * @param resolution the resolution in dpi (dots per inch) + * @return the value in pixels + */ + public static double mm2px(double mm, int resolution) { + return mm2in(mm) * resolution; + } + + /** + * Converts millipoints (mpt) to pixels (px) + * @param mpt the value in mpt + * @param resolution the resolution in dpi (dots per inch) + * @return the value in pixels + */ + public static double mpt2px(double mpt, int resolution) { + return mpt2in(mpt) * resolution; + } + + /** + * Converts a millipoint-based transformation matrix to points. + * @param at a millipoint-based transformation matrix + * @return a point-based transformation matrix + */ + public static AffineTransform mptToPt(AffineTransform at) { + double[] matrix = new double[6]; + at.getMatrix(matrix); + //Convert to points + matrix[4] = matrix[4] / 1000; + matrix[5] = matrix[5] / 1000; + return new AffineTransform(matrix); + } + + /** + * Converts a point-based transformation matrix to millipoints. + * @param at a point-based transformation matrix + * @return a millipoint-based transformation matrix + */ + public static AffineTransform ptToMpt(AffineTransform at) { + double[] matrix = new double[6]; + at.getMatrix(matrix); + //Convert to millipoints + matrix[4] = matrix[4] * 1000; + matrix[5] = matrix[5] * 1000; + return new AffineTransform(matrix); + } + + /** + * Convert the given unit length to a dimensionless integer representing + * a whole number of base units (milli-points). + * + * @param value input unit value + * @return int millipoints + */ + public static int convert(String value) { + double retValue = 0; + if (value != null) { + if (value.toLowerCase(Locale.getDefault()).indexOf(PX) >= 0) { + retValue = Double.parseDouble(value.substring(0, value.length() - 2)); + retValue *= 1000; + } else if (value.toLowerCase(Locale.getDefault()).indexOf(INCH) >= 0) { + retValue = Double.parseDouble(value.substring(0, value.length() - 2)); + retValue *= 72000; + } else if (value.toLowerCase(Locale.getDefault()).indexOf(CM) >= 0) { + retValue = Double.parseDouble(value.substring(0, value.length() - 2)); + retValue *= 28346.4567; + } else if (value.toLowerCase(Locale.getDefault()).indexOf(MM) >= 0) { + retValue = Double.parseDouble(value.substring(0, value.length() - 2)); + retValue *= 2834.64567; + } else if (value.toLowerCase(Locale.getDefault()).indexOf(MPT) >= 0) { + retValue = Double.parseDouble(value.substring(0, value.length() - 3)); + } else if (value.toLowerCase(Locale.getDefault()).indexOf(POINT) >= 0) { + retValue = Double.parseDouble(value.substring(0, value.length() - 2)); + retValue *= 1000; + } else if (value.toLowerCase(Locale.getDefault()).indexOf(PICA) >= 0) { + retValue = Double.parseDouble(value.substring(0, value.length() - 2)); + retValue *= 12000; + } + } + return (int)retValue; + } +} diff --git a/src/main/java/org/apache/xmlgraphics/util/WriterOutputStream.java b/src/main/java/org/apache/xmlgraphics/util/WriterOutputStream.java new file mode 100644 index 0000000..1fdd01e --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/util/WriterOutputStream.java @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: WriterOutputStream.java 1681108 2015-05-22 13:26:12Z ssteiner $ */ + +package org.apache.xmlgraphics.util; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.Writer; + +/** + * An OutputStream wrapper for a Writer. + */ +public class WriterOutputStream extends OutputStream { + + private Writer writer; + private String encoding; + + /** + * Creates a new WriterOutputStream. + * @param writer the Writer to write to + */ + public WriterOutputStream(Writer writer) { + this(writer, null); + } + + /** + * Creates a new WriterOutputStream. + * @param writer the Writer to write to + * @param encoding the encoding to use, or null if the default encoding should be used + */ + public WriterOutputStream(Writer writer, String encoding) { + this.writer = writer; + this.encoding = encoding; + } + + /** + * {@inheritDoc} + */ + public void close() throws IOException { + writer.close(); + } + + /** + * {@inheritDoc} + */ + public void flush() throws IOException { + writer.flush(); + } + + /** + * {@inheritDoc} + */ + public void write(byte[] buf, int offset, int length) throws IOException { + if (encoding != null) { + writer.write(new String(buf, offset, length, encoding)); + } else { + writer.write(new String(buf, offset, length, "UTF-8")); + } + } + + /** + * {@inheritDoc} + */ + public void write(byte[] buf) throws IOException { + write(buf, 0, buf.length); + } + + /** + * {@inheritDoc} + */ + public void write(int b) throws IOException { + write(new byte[] {(byte)b}); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/util/XMLizable.java b/src/main/java/org/apache/xmlgraphics/util/XMLizable.java new file mode 100644 index 0000000..1907b24 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/util/XMLizable.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: XMLizable.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.util; + +/* + * Copied from Apache Excalibur: + * https://svn.apache.org/repos/asf/excalibur/trunk/components/xmlutil/ + * src/java/org/apache/excalibur/xml/sax/XMLizable.java + */ + +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; + +/** + * This interface can be implemented by classes willing to provide an XML representation + * of their current state as SAX events. + */ +public interface XMLizable { + + /** + * Generates SAX events representing the object's state. + * @param handler ContentHandler instance to send the SAX events to + * @throws SAXException if there's a problem generating the SAX events + */ + void toSAX(ContentHandler handler) throws SAXException; + +} diff --git a/src/main/java/org/apache/xmlgraphics/util/dijkstra/DefaultEdgeDirectory.java b/src/main/java/org/apache/xmlgraphics/util/dijkstra/DefaultEdgeDirectory.java new file mode 100644 index 0000000..b4d0fe7 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/util/dijkstra/DefaultEdgeDirectory.java @@ -0,0 +1,109 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DefaultEdgeDirectory.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.util.dijkstra; + +import java.util.Collections; +import java.util.Iterator; +import java.util.Map; + +/** + * Default implementation of an edge directory for the {@link DijkstraAlgorithm}. + */ +public class DefaultEdgeDirectory implements EdgeDirectory { + + /** The directory of edges */ + private Map edges = new java.util.HashMap(); + //Map> + + /** + * Adds a new edge between two vertices. + * @param edge the new edge + */ + public void addEdge(Edge edge) { + Map directEdges = (Map)edges.get(edge.getStart()); + if (directEdges == null) { + directEdges = new java.util.HashMap(); + edges.put(edge.getStart(), directEdges); + } + directEdges.put(edge.getEnd(), edge); + } + + /** {@inheritDoc} */ + public int getPenalty(Vertex start, Vertex end) { + Map edgeMap = (Map)edges.get(start); + if (edgeMap != null) { + Edge route = (Edge)edgeMap.get(end); + if (route != null) { + int penalty = route.getPenalty(); + if (penalty < 0) { + throw new IllegalStateException("Penalty must not be negative"); + } + return penalty; + } + } + return 0; + } + + /** {@inheritDoc} */ + public Iterator getDestinations(Vertex origin) { + Map directRoutes = (Map)edges.get(origin); + if (directRoutes != null) { + Iterator iter = directRoutes.keySet().iterator(); + return iter; + } + return Collections.EMPTY_LIST.iterator(); + } + + /** + * Returns an iterator over all edges with the given origin. + * @param origin the origin + * @return an iterator over Edge instances + */ + public Iterator getEdges(Vertex origin) { + Map directRoutes = (Map)edges.get(origin); + if (directRoutes != null) { + Iterator iter = directRoutes.values().iterator(); + return iter; + } + return Collections.EMPTY_LIST.iterator(); + } + + /** + * Returns the best edge (the edge with the lowest penalty) between two given vertices. + * @param start the start vertex + * @param end the end vertex + * @return the best vertex or null if none is found + */ + public Edge getBestEdge(Vertex start, Vertex end) { + Edge best = null; + Iterator iter = getEdges(start); + while (iter.hasNext()) { + Edge edge = (Edge)iter.next(); + if (edge.getEnd().equals(end)) { + if (best == null || edge.getPenalty() < best.getPenalty()) { + best = edge; + } + } + } + return best; + } + + +} diff --git a/src/main/java/org/apache/xmlgraphics/util/dijkstra/DijkstraAlgorithm.java b/src/main/java/org/apache/xmlgraphics/util/dijkstra/DijkstraAlgorithm.java new file mode 100644 index 0000000..c35b1f4 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/util/dijkstra/DijkstraAlgorithm.java @@ -0,0 +1,215 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DijkstraAlgorithm.java 1681108 2015-05-22 13:26:12Z ssteiner $ */ + +package org.apache.xmlgraphics.util.dijkstra; + +import java.util.Comparator; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +/** + * This is an implementation of Dijkstra's algorithm to find the shortest path for a directed + * graph with non-negative edge weights. + * @see WikiPedia on Dijkstra's + * Algorithm + */ +public class DijkstraAlgorithm { + + /** Infinity value for distances. */ + public static final int INFINITE = Integer.MAX_VALUE; + + /** Compares penalties between two possible destinations. */ + private final Comparator penaltyComparator = new Comparator() { + public int compare(Object left, Object right) { + int leftPenalty = getLowestPenalty((Vertex)left); + int rightPenalty = getLowestPenalty((Vertex)right); + if (leftPenalty < rightPenalty) { + return -1; + } else if (leftPenalty == rightPenalty) { + return ((Comparable)left).compareTo(right); + } else { + return 1; + } + } + }; + + /** The directory of edges */ + private EdgeDirectory edgeDirectory; + + /** The priority queue for all vertices under inspection, ordered by penalties/distances. */ + private TreeSet priorityQueue = new TreeSet(penaltyComparator); + //Set + + /** The set of vertices for which the lowest penalty has been found. */ + private Set finishedVertices = new java.util.HashSet(); + //Set + + /** The currently known lowest penalties for all vertices. */ + private Map lowestPenalties = new java.util.HashMap(); + //Map + + /** Map of all predecessors in the spanning tree of best routes. */ + private Map predecessors = new java.util.HashMap(); + //Map + + /** + * Main Constructor. + * @param edgeDirectory the edge directory this instance should work on + */ + public DijkstraAlgorithm(EdgeDirectory edgeDirectory) { + this.edgeDirectory = edgeDirectory; + } + + /** + * Returns the penalty between two vertices. + * @param start the start vertex + * @param end the end vertex + * @return the penalty between two vertices, or 0 if no single edge between the two vertices + * exists. + */ + protected int getPenalty(Vertex start, Vertex end) { + return this.edgeDirectory.getPenalty(start, end); + } + + /** + * Returns an iterator over all valid destinations for a given vertex. + * @param origin the origin from which to search for destinations + * @return the iterator over all valid destinations for a given vertex + */ + protected Iterator getDestinations(Vertex origin) { + return this.edgeDirectory.getDestinations(origin); + } + + private void reset() { + finishedVertices.clear(); + priorityQueue.clear(); + + lowestPenalties.clear(); + predecessors.clear(); + } + + /** + * Run Dijkstra's shortest path algorithm. After this method is finished you can use + * {@link #getPredecessor(Vertex)} to reconstruct the best/shortest path starting from the + * destination backwards. + * @param start the starting vertex + * @param destination the destination vertex. + */ + public void execute(Vertex start, Vertex destination) { + if (start == null || destination == null) { + throw new NullPointerException("start and destination may not be null"); + } + + reset(); + setShortestDistance(start, 0); + priorityQueue.add(start); + + // the current node + Vertex u; + + // extract the vertex with the shortest distance + while (priorityQueue.size() > 0) { + u = (Vertex)priorityQueue.first(); + priorityQueue.remove(u); + + if (destination.equals(u)) { + //Destination reached + break; + } + + finishedVertices.add(u); + relax(u); + } + } + + /** + * Compute new lowest penalties for neighboring vertices. Update the lowest penalties and the + * predecessor map if a better solution is found. + * @param u the vertex to process + */ + private void relax(Vertex u) { + Iterator iter = getDestinations(u); + while (iter.hasNext()) { + Vertex v = (Vertex)iter.next(); + // skip node already settled + if (isFinished(v)) { + continue; + } + + int shortDist = getLowestPenalty(u) + getPenalty(u, v); + + if (shortDist < getLowestPenalty(v)) { + // assign new shortest distance and mark unsettled + setShortestDistance(v, shortDist); + + // assign predecessor in shortest path + setPredecessor(v, u); + } + } + } + + private void setPredecessor(Vertex a, Vertex b) { + predecessors.put(a, b); + } + + /** + * Indicates whether a shortest route to a vertex has been found. + * @param v the vertex + * @return true if the shortest route to this vertex has been found. + */ + private boolean isFinished(Vertex v) { + return finishedVertices.contains(v); + } + + private void setShortestDistance(Vertex vertex, int distance) { + //Remove so it is inserted at the right position after the lowest penalty changes for this + //vertex. + priorityQueue.remove(vertex); + + //Update the lowest penalty. + lowestPenalties.put(vertex, distance); + + //Insert the vertex again at the new position based on the lowest penalty + priorityQueue.add(vertex); + } + + /** + * Returns the lowest penalty from the start point to a given vertex. + * @param vertex the vertex + * @return the lowest penalty or {@link DijkstraAlgorithm#INFINITE} if there is no route to + * the destination. + */ + public int getLowestPenalty(Vertex vertex) { + Integer d = ((Integer)lowestPenalties.get(vertex)); + return (d == null) ? INFINITE : d; + } + + /** + * Returns the vertex's predecessor on the shortest path. + * @param vertex the vertex for which to find the predecessor + * @return the vertex's predecessor on the shortest path, or + * null if there is no route to the destination. + */ + public Vertex getPredecessor(Vertex vertex) { + return (Vertex)predecessors.get(vertex); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/util/dijkstra/Edge.java b/src/main/java/org/apache/xmlgraphics/util/dijkstra/Edge.java new file mode 100644 index 0000000..9f0db6d --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/util/dijkstra/Edge.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: Edge.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.util.dijkstra; + +/** + * Represents an edge (or direct route between two points) for the {@link DijkstraAlgorithm}. + * Implement this class to hold the start and end vertex for an edge and implement the + * getPenalty() method. + */ +public interface Edge { + + /** + * Returns the start vertex of the edge. + * @return the start vertex + */ + Vertex getStart(); + + /** + * Returns the end vertex of the edge. + * @return the end vertex + */ + Vertex getEnd(); + + /** + * Returns the penalty (or distance) for this edge. + * @return the penalty value (must be non-negative) + */ + int getPenalty(); + +} diff --git a/src/main/java/org/apache/xmlgraphics/util/dijkstra/EdgeDirectory.java b/src/main/java/org/apache/xmlgraphics/util/dijkstra/EdgeDirectory.java new file mode 100644 index 0000000..d6b88f8 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/util/dijkstra/EdgeDirectory.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: EdgeDirectory.java 1345683 2012-06-03 14:50:33Z gadams $ */ + +package org.apache.xmlgraphics.util.dijkstra; + +import java.util.Iterator; + +/** + * Represents a directory of edges for use by the {@link DijkstraAlgorithm}. + */ +public interface EdgeDirectory { + + /** + * Returns the penalty between two vertices. + * @param start the start vertex + * @param end the end vertex + * @return the penalty between two vertices, or 0 if no single edge between the two vertices + * exists. + */ + int getPenalty(Vertex start, Vertex end); + + /** + * Returns an iterator over all valid destinations for a given vertex. + * @param origin the origin from which to search for destinations + * @return the iterator over all valid destinations for a given vertex + */ + Iterator getDestinations(Vertex origin); + +} diff --git a/src/main/java/org/apache/xmlgraphics/util/dijkstra/Vertex.java b/src/main/java/org/apache/xmlgraphics/util/dijkstra/Vertex.java new file mode 100644 index 0000000..6966061 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/util/dijkstra/Vertex.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: Vertex.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.util.dijkstra; + +/** + * Represents a vertex to be used by {@link DijkstraAlgorithm}. If you want to represent a city, + * you can do "public class City implements Vertex". The purpose of this interface is to make + * sure the Vertex implementation implements the Comparable interface so the sorting order is + * well-defined even when two vertices have the same penalty/distance from an origin point. + * Therefore, make sure you implement the compareTo(Object) and + * equals(Object) methods. + */ +public interface Vertex extends Comparable { + +} diff --git a/src/main/java/org/apache/xmlgraphics/util/dijkstra/package.html b/src/main/java/org/apache/xmlgraphics/util/dijkstra/package.html new file mode 100644 index 0000000..b6d1e75 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/util/dijkstra/package.html @@ -0,0 +1,26 @@ + + + +org.apache.xmlgraphics.util.dijkstra Package + +

    + Contains an implementation of Dijkstra's shortest path algorithm. The package is primarily used + by the image loader package (org.apache.xmlgraphics.image.loader). +

    + + \ No newline at end of file diff --git a/src/main/java/org/apache/xmlgraphics/util/i18n/ExtendedLocalizable.java b/src/main/java/org/apache/xmlgraphics/util/i18n/ExtendedLocalizable.java new file mode 100644 index 0000000..f735cc7 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/util/i18n/ExtendedLocalizable.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ExtendedLocalizable.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.util.i18n; + +import java.util.Locale; +import java.util.ResourceBundle; + +/** + * This interface provides much more control over internationalization + * than the Localizable interface. + * + * @version $Id: ExtendedLocalizable.java 1732018 2016-02-24 04:51:06Z gadams $ + * + * Originally authored by Stephane Hillion. + */ +public interface ExtendedLocalizable extends Localizable { + /** + * Sets the group to which this object belongs. + */ + void setLocaleGroup(LocaleGroup lg); + + /** + * Returns the group to which this object belongs. + */ + LocaleGroup getLocaleGroup(); + + /** + * Sets the default locale for all the instances of this class in + * the same LocaleGroup. + */ + void setDefaultLocale(Locale l); + + /** + * Gets the current default locale in the LocaleGroup. + */ + Locale getDefaultLocale(); + + /** + * Returns the current resource bundle. Getting this object gives access + * to the keys in the bundle, raw string resources, arrays of raw string + * resources and object resources. + */ + ResourceBundle getResourceBundle(); +} diff --git a/src/main/java/org/apache/xmlgraphics/util/i18n/LocaleGroup.java b/src/main/java/org/apache/xmlgraphics/util/i18n/LocaleGroup.java new file mode 100644 index 0000000..1cb3f51 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/util/i18n/LocaleGroup.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: LocaleGroup.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.util.i18n; + +import java.util.Locale; + +/** + * This class represents a group of ExtendedLocalizable objects which + * have a shared default locale. + * + * @version $Id: LocaleGroup.java 1732018 2016-02-24 04:51:06Z gadams $ + * + * Originally authored by Stephane Hillion. + */ +public class LocaleGroup { + /** + * The default group. + */ + public static final LocaleGroup DEFAULT = new LocaleGroup(); + + /** + * The shared Locale. + */ + protected Locale locale; + + /** + * Sets the default locale for all the instances of ExtendedLocalizable + * in this group. + */ + public void setLocale(Locale l) { + locale = l; + } + + /** + * Gets the current default locale in this group, or null. + */ + public Locale getLocale() { + return locale; + } +} diff --git a/src/main/java/org/apache/xmlgraphics/util/i18n/Localizable.java b/src/main/java/org/apache/xmlgraphics/util/i18n/Localizable.java new file mode 100644 index 0000000..e84c5fb --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/util/i18n/Localizable.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: Localizable.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.util.i18n; + +import java.util.Locale; +import java.util.MissingResourceException; + +/** + * This interface must be implemented by the classes which must provide a + * way to override the default locale. + * + * @version $Id: Localizable.java 1732018 2016-02-24 04:51:06Z gadams $ + * + * Originally authored by Stephane Hillion. + */ +public interface Localizable { + /** + * Provides a way to the user to specify a locale which override the + * default one. If null is passed to this method, the used locale + * becomes the global one. + * @param l The locale to set. + */ + void setLocale(Locale l); + + /** + * Returns the current locale or null if the locale currently used is + * the default one. + */ + Locale getLocale(); + + /** + * Creates and returns a localized message, given the key of the message + * in the resource bundle and the message parameters. + * The messages in the resource bundle must have the syntax described in + * the java.text.MessageFormat class documentation. + * @param key The key used to retreive the message from the resource + * bundle. + * @param args The objects that compose the message. + * @exception MissingResourceException if the key is not in the bundle. + */ + String formatMessage(String key, Object[] args) + throws MissingResourceException; +} diff --git a/src/main/java/org/apache/xmlgraphics/util/i18n/LocalizableSupport.java b/src/main/java/org/apache/xmlgraphics/util/i18n/LocalizableSupport.java new file mode 100644 index 0000000..3bbd12e --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/util/i18n/LocalizableSupport.java @@ -0,0 +1,249 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: LocalizableSupport.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.util.i18n; + +import java.text.MessageFormat; +import java.util.Locale; +import java.util.ResourceBundle; + +// CSOFF: InnerAssignment + +/** + * This class provides a default implementation of the Localizable interface. + * You can use it as a base class or as a member field and delegates various + * work to it.

    + * For example, to implement Localizable, the following code can be used: + *

    + *  package mypackage;
    + *  ...
    + *  public class MyClass implements Localizable {
    + *      // This code fragment requires a file named
    + *      // 'mypackage/resources/Messages.properties', or a
    + *      // 'mypackage.resources.Messages' class which extends
    + *      // java.util.ResourceBundle, accessible using the current
    + *      // classpath.
    + *      LocalizableSupport localizableSupport =
    + *          new LocalizableSupport("mypackage.resources.Messages");
    + *
    + *      public void setLocale(Locale l) {
    + *          localizableSupport.setLocale(l);
    + *      }
    + *      public Local getLocale() {
    + *          return localizableSupport.getLocale();
    + *      }
    + *      public String formatMessage(String key, Object[] args) {
    + *          return localizableSupport.formatMessage(key, args);
    + *      }
    + *  }
    + * 
    + * The algorithm for the Locale lookup in a LocalizableSupport object is: + *
      + *
    • + * if a Locale has been set by a call to setLocale(), use this Locale, + * else, + *
    • + *
    • + * if a Locale has been set by a call to the setDefaultLocale() method + * of a LocalizableSupport object in the current LocaleGroup, use this + * Locale, else, + *
    • + *
    • + * use the object returned by Locale.getDefault() (and set by + * Locale.setDefault()). + *
    • + *
    + * This offers the possibility to have a different Locale for each object, + * a Locale for a group of object and/or a Locale for the JVM instance. + *

    + * Note: if no group is specified a LocalizableSupport object belongs to a + * default group common to each instance of LocalizableSupport. + * + * @version $Id: LocalizableSupport.java 1732018 2016-02-24 04:51:06Z gadams $ + * + * Originally authored by Stephane Hillion. + */ +public class LocalizableSupport implements Localizable { + /** + * The locale group to which this object belongs. + */ + protected LocaleGroup localeGroup = LocaleGroup.DEFAULT; + + /** + * The resource bundle classname. + */ + protected String bundleName; + + /** + * The classloader to use to create the resource bundle. + */ + protected ClassLoader classLoader; + + /** + * The current locale. + */ + protected Locale locale; + + /** + * The locale in use. + */ + protected Locale usedLocale; + + /** + * The resources + */ + protected ResourceBundle resourceBundle; + + /** + * Same as LocalizableSupport(s, null). + */ + public LocalizableSupport(String s) { + this(s, null); + } + + /** + * Creates a new Localizable object. + * The resource bundle class name is required allows the use of custom + * classes of resource bundles. + * @param s must be the name of the class to use to get the appropriate + * resource bundle given the current locale. + * @param cl is the classloader used to create the resource bundle, + * or null. + * @see java.util.ResourceBundle + */ + public LocalizableSupport(String s, ClassLoader cl) { + bundleName = s; + classLoader = cl; + } + + /** + * Implements {@link org.apache.xmlgraphics.util.i18n.Localizable#setLocale(Locale)}. + */ + public void setLocale(Locale l) { + if (locale != l) { + locale = l; + resourceBundle = null; + } + } + + /** + * Implements {@link org.apache.xmlgraphics.util.i18n.Localizable#getLocale()}. + */ + public Locale getLocale() { + return locale; + } + + /** + * Implements {@link + * org.apache.xmlgraphics.util.i18n.ExtendedLocalizable#setLocaleGroup(LocaleGroup)}. + */ + public void setLocaleGroup(LocaleGroup lg) { + localeGroup = lg; + } + + /** + * Implements {@link + * org.apache.xmlgraphics.util.i18n.ExtendedLocalizable#getLocaleGroup()}. + */ + public LocaleGroup getLocaleGroup() { + return localeGroup; + } + + /** + * Implements {@link + * org.apache.xmlgraphics.util.i18n.ExtendedLocalizable#setDefaultLocale(Locale)}. + * Later invocations of the instance methods will lead to update the + * resource bundle used. + */ + public void setDefaultLocale(Locale l) { + localeGroup.setLocale(l); + } + + /** + * Implements {@link + * org.apache.xmlgraphics.util.i18n.ExtendedLocalizable#getDefaultLocale()}. + */ + public Locale getDefaultLocale() { + return localeGroup.getLocale(); + } + + /** + * Implements {@link + * org.apache.xmlgraphics.util.i18n.Localizable#formatMessage(String,Object[])}. + */ + public String formatMessage(String key, Object[] args) { + getResourceBundle(); + return MessageFormat.format(resourceBundle.getString(key), args); + } + + /** + * Implements {@link + * org.apache.xmlgraphics.util.i18n.ExtendedLocalizable#getResourceBundle()}. + */ + public ResourceBundle getResourceBundle() { + Locale l; + + if (resourceBundle == null) { + if (locale == null) { + if ((l = localeGroup.getLocale()) == null) { + usedLocale = Locale.getDefault(); + } else { + usedLocale = l; + } + } else { + usedLocale = locale; + } + if (classLoader == null) { + resourceBundle = ResourceBundle.getBundle(bundleName, + usedLocale); + } else { + resourceBundle = ResourceBundle.getBundle(bundleName, + usedLocale, + classLoader); + } + } else if (locale == null) { + // Check for group Locale and JVM default locale changes. + if ((l = localeGroup.getLocale()) == null) { + if (usedLocale != (l = Locale.getDefault())) { + usedLocale = l; + if (classLoader == null) { + resourceBundle = ResourceBundle.getBundle(bundleName, + usedLocale); + } else { + resourceBundle = ResourceBundle.getBundle(bundleName, + usedLocale, + classLoader); + } + } + } else if (usedLocale != l) { + usedLocale = l; + if (classLoader == null) { + resourceBundle = ResourceBundle.getBundle(bundleName, + usedLocale); + } else { + resourceBundle = ResourceBundle.getBundle(bundleName, + usedLocale, + classLoader); + } + } + } + + return resourceBundle; + } +} diff --git a/src/main/java/org/apache/xmlgraphics/util/i18n/package.html b/src/main/java/org/apache/xmlgraphics/util/i18n/package.html new file mode 100644 index 0000000..94964a9 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/util/i18n/package.html @@ -0,0 +1,13 @@ + + + + Provides utility classes for internationalization. +

    + The responsibility of the i18n package is to provide a facade for + the various internationalization utility classes of the standard + Java API. The API should be used for compositing string messages in + a language-neutral way. +

    + + diff --git a/src/main/java/org/apache/xmlgraphics/util/io/ASCII85Constants.java b/src/main/java/org/apache/xmlgraphics/util/io/ASCII85Constants.java new file mode 100644 index 0000000..dedb9a5 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/util/io/ASCII85Constants.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ASCII85Constants.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.util.io; + +/** + * This interface defines constants used by the ASCII85 filters. + * + * @version $Id: ASCII85Constants.java 1732018 2016-02-24 04:51:06Z gadams $ + */ +public interface ASCII85Constants { + + /** Special character "z" stands for four NULL bytes (short-cut for !!!!!) */ + int ZERO = 0x7A; //"z" + /** ZERO as a byte array */ + byte[] ZERO_ARRAY = {(byte)ZERO}; + /** The start index for ASCII85 characters (!) */ + int START = 0x21; //"!" + /** The end index for ASCII85 characters (u) */ + int END = 0x75; //"u" + /** The EOL indicator (LF) */ + int EOL = 0x0A; //"\n" + /** The EOD (end of data) indicator */ + byte[] EOD = {0x7E, 0x3E}; //"~>" + + /** Array of powers of 85 (4, 3, 2, 1, 0) */ + long[] POW85 = new long[] {85 * 85 * 85 * 85, + 85 * 85 * 85, + 85 * 85, + 85, + 1}; + + /* + long BASE85_4 = 85; + long BASE85_3 = BASE85_4 * BASE85_4; + long BASE85_2 = BASE85_3 * BASE85_4; + long BASE85_1 = BASE85_2 * BASE85_4; + */ + +} + + diff --git a/src/main/java/org/apache/xmlgraphics/util/io/ASCII85InputStream.java b/src/main/java/org/apache/xmlgraphics/util/io/ASCII85InputStream.java new file mode 100644 index 0000000..1c88f27 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/util/io/ASCII85InputStream.java @@ -0,0 +1,162 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ASCII85InputStream.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.util.io; + +import java.io.IOException; +import java.io.InputStream; + +/** + * This class applies a ASCII85 decoding to the stream. + *

    + * The class is derived from InputStream instead of FilteredInputStream because + * we can use the read(byte[], int, int) method from InputStream which simply + * delegates to read(). This makes the implementation easier. + *

    + * The filter is described in chapter 3.13.3 of the PostScript Language + * Reference (third edition). + * + * @version $Id: ASCII85InputStream.java 1732018 2016-02-24 04:51:06Z gadams $ + */ +public class ASCII85InputStream extends InputStream + implements ASCII85Constants { + + private InputStream in; + private boolean eodReached; + private int[] b = new int[4]; //decoded + private int bSize; + private int bIndex; + + /** @see java.io.FilterInputStream **/ + public ASCII85InputStream(InputStream in) { + super(); + this.in = in; + } + + /** @see java.io.FilterInputStream **/ + public int read() throws IOException { + //Check if we need to read the next tuple + if (bIndex >= bSize) { + if (eodReached) { + return -1; + } + readNextTuple(); + if (bSize == 0) { + if (!eodReached) { + throw new IllegalStateException("Internal error"); + } + return -1; + } + } + int result = b[bIndex]; + result = (result < 0 ? 256 + result : result); + bIndex++; + return result; + } + + private int filteredRead() throws IOException { + int buf; + while (true) { + buf = in.read(); + switch (buf) { + case 0: //null + case 9: //tab + case 10: //LF + case 12: //FF + case 13: //CR + case 32: //space + continue; //ignore + case ZERO: + case 126: //= EOD[0] = '~' + return buf; + default: + if ((buf >= START) && (buf <= END)) { + return buf; + } else { + throw new IOException("Illegal character detected: " + buf); + } + } + } + } + + private void handleEOD() throws IOException { + final int buf = in.read(); + if (buf != EOD[1]) { + throw new IOException("'>' expected after '~' (EOD)"); + } + eodReached = true; + bSize = 0; + bIndex = 0; + } + + private void readNextTuple() throws IOException { + int buf; + long tuple = 0; + //Read ahead and check for special "z" + buf = filteredRead(); + if (buf == ZERO) { + java.util.Arrays.fill(b, 0); + bSize = 4; + bIndex = 0; + } else if (buf == EOD[0]) { + handleEOD(); + } else { + int cIndex = 0; + tuple = (buf - START) * POW85[cIndex]; + cIndex++; + while (cIndex < 5) { + buf = filteredRead(); + if (buf == EOD[0]) { + handleEOD(); + break; + } else if (buf == ZERO) { + //Violation 2 + throw new IOException("Illegal 'z' within tuple"); + } else { + tuple += (buf - START) * POW85[cIndex]; + cIndex++; + } + } + int cSize = cIndex; + if (cSize == 1) { + //Violation 3 + throw new IOException("Only one character in tuple"); + } + //Handle optional, trailing, incomplete tuple + while (cIndex < 5) { + tuple += POW85[cIndex - 1]; + cIndex++; + } + if (tuple > (2L << 31) - 1) { + //Violation 1 + throw new IOException("Illegal tuple (> 2^32 - 1)"); + } + //Convert tuple + b[0] = (byte)((tuple >> 24) & 0xFF); + b[1] = (byte)((tuple >> 16) & 0xFF); + b[2] = (byte)((tuple >> 8) & 0xFF); + b[3] = (byte)((tuple) & 0xFF); + bSize = cSize - 1; + bIndex = 0; + } + } + +} + + diff --git a/src/main/java/org/apache/xmlgraphics/util/io/ASCII85OutputStream.java b/src/main/java/org/apache/xmlgraphics/util/io/ASCII85OutputStream.java new file mode 100644 index 0000000..81278ca --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/util/io/ASCII85OutputStream.java @@ -0,0 +1,222 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ASCII85OutputStream.java 1804124 2017-08-04 14:13:54Z ssteiner $ */ + +package org.apache.xmlgraphics.util.io; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** + * This class applies a ASCII85 encoding to the stream. + * + * @version $Id: ASCII85OutputStream.java 1804124 2017-08-04 14:13:54Z ssteiner $ + */ +public class ASCII85OutputStream extends FilterOutputStream + implements ASCII85Constants, Finalizable { + + private static final boolean DEBUG = false; + + private int pos; + private long buffer; + private int posinline; + private int bw; + + /** @see java.io.FilterOutputStream **/ + public ASCII85OutputStream(OutputStream out) { + super(out); + } + + /** @see java.io.FilterOutputStream **/ + public void write(int b) throws IOException { + if (pos == 0) { + buffer += (b << 24) & 0xff000000L; + } else if (pos == 1) { + buffer += (b << 16) & 0xff0000L; + } else if (pos == 2) { + buffer += (b << 8) & 0xff00L; + } else { + buffer += b & 0xffL; + } + pos++; + + if (pos > 3) { + checkedWrite(convertWord(buffer)); + buffer = 0; + pos = 0; + } + } + + /* UNUSED ATM + private void checkedWrite(int b) throws IOException { + if (posinline == 80) { + out.write(EOL); bw++; + posinline = 0; + } + checkedWrite(b); + posinline++; + bw++; + }*/ + + private void checkedWrite(byte[] buf) throws IOException { + checkedWrite(buf, buf.length, false); + } + + private void checkedWrite(byte[] buf, boolean nosplit) throws IOException { + checkedWrite(buf, buf.length, nosplit); + } + + private void checkedWrite(byte[] buf , int len) throws IOException { + checkedWrite(buf, len, false); + } + + private void checkedWrite(byte[] buf , int len, boolean nosplit) throws IOException { + if (posinline + len > 80) { + int firstpart = (nosplit ? 0 : len - (posinline + len - 80)); + if (firstpart > 0) { + out.write(buf, 0, firstpart); + } + out.write(EOL); + bw++; + int rest = len - firstpart; + if (rest > 0) { + out.write(buf, firstpart, rest); + } + posinline = rest; + } else { + out.write(buf, 0, len); + posinline += len; + } + bw += len; + } + + /** + * This converts a 32 bit value (4 bytes) into 5 bytes using base 85. + * each byte in the result starts with zero at the '!' character so + * the resulting base85 number fits into printable ascii chars + * + * @param word the 32 bit unsigned (hence the long datatype) word + * @return 5 bytes (or a single byte of the 'z' character for word + * values of 0) + */ + private byte[] convertWord(long word) { + word = word & 0xffffffff; + + if (word == 0) { + return ZERO_ARRAY; + } else { + if (word < 0) { + word = -word; + } + byte c1 = + (byte)((word + / POW85[0]) & 0xFF); + byte c2 = + (byte)(((word - (c1 * POW85[0])) + / POW85[1]) & 0xFF); + byte c3 = + (byte)(((word - (c1 * POW85[0]) + - (c2 * POW85[1])) + / POW85[2]) & 0xFF); + byte c4 = + (byte)(((word - (c1 * POW85[0]) + - (c2 * POW85[1]) + - (c3 * POW85[2])) + / POW85[3]) & 0xFF); + byte c5 = + (byte)(((word - (c1 * POW85[0]) + - (c2 * POW85[1]) + - (c3 * POW85[2]) + - (c4 * POW85[3]))) + & 0xFF); + + byte[] ret = { + (byte)(c1 + START), (byte)(c2 + START), + (byte)(c3 + START), (byte)(c4 + START), + (byte)(c5 + START) + }; + + if (DEBUG) { + for (byte aRet : ret) { + if (aRet < 33 || aRet > 117) { + System.out.println("Illegal char value " + + (int) aRet); + } + } + } + return ret; + } + } + + /** @see Finalizable **/ + public void finalizeStream() throws IOException { + // now take care of the trailing few bytes. + // with n leftover bytes, we append 0 bytes to make a full group of 4 + // then convert like normal (except not applying the special zero rule) + // and write out the first n+1 bytes from the result + if (pos > 0) { + int rest = pos; + /* + byte[] lastdata = new byte[4]; + int i = 0; + for (int j = 0; j < 4; j++) { + if (j < rest) { + lastdata[j] = data[i++]; + } else { + lastdata[j] = 0; + } + } + + long val = ((lastdata[0] << 24) & 0xff000000L) + + ((lastdata[1] << 16) & 0xff0000L) + + ((lastdata[2] << 8) & 0xff00L) + + (lastdata[3] & 0xffL); + */ + + byte[] conv; + // special rule for handling zeros at the end + if (buffer != 0) { + conv = convertWord(buffer); + } else { + conv = new byte[5]; + for (int j = 0; j < 5; j++) { + conv[j] = (byte)'!'; + } + } + // assert rest+1 <= 5 + checkedWrite(conv, rest + 1); + } + // finally write the two character end of data marker + checkedWrite(EOD, true); + + flush(); + if (out instanceof Finalizable) { + ((Finalizable)out).finalizeStream(); + } + } + + /** @see java.io.FilterOutputStream **/ + public void close() throws IOException { + finalizeStream(); + super.close(); + } + +} + + diff --git a/src/main/java/org/apache/xmlgraphics/util/io/ASCIIHexOutputStream.java b/src/main/java/org/apache/xmlgraphics/util/io/ASCIIHexOutputStream.java new file mode 100644 index 0000000..e1099c7 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/util/io/ASCIIHexOutputStream.java @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ASCIIHexOutputStream.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.util.io; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** + * This class applies a ASCII Hex encoding to the stream. + * + * @version $Id: ASCIIHexOutputStream.java 1732018 2016-02-24 04:51:06Z gadams $ + */ +public class ASCIIHexOutputStream extends FilterOutputStream + implements Finalizable { + + private static final int EOL = 0x0A; //"\n" + private static final int EOD = 0x3E; //">" + private static final int ZERO = 0x30; //"0" + private static final int NINE = 0x39; //"9" + private static final int A = 0x41; //"A" + private static final int ADIFF = A - NINE - 1; + + private int posinline; + + + /** @see java.io.FilterOutputStream **/ + public ASCIIHexOutputStream(OutputStream out) { + super(out); + } + + + /** @see java.io.FilterOutputStream **/ + public void write(int b) throws IOException { + b &= 0xFF; + + int digit1 = ((b & 0xF0) >> 4) + ZERO; + if (digit1 > NINE) { + digit1 += ADIFF; + } + out.write(digit1); + + int digit2 = (b & 0x0F) + ZERO; + if (digit2 > NINE) { + digit2 += ADIFF; + } + out.write(digit2); + + posinline++; + checkLineWrap(); + } + + + private void checkLineWrap() throws IOException { + //Maximum line length is 80 characters + if (posinline >= 40) { + out.write(EOL); + posinline = 0; + } + } + + + /** @see Finalizable **/ + public void finalizeStream() throws IOException { + checkLineWrap(); + //Write closing character ">" + super.write(EOD); + + flush(); + if (out instanceof Finalizable) { + ((Finalizable) out).finalizeStream(); + } + } + + + /** @see java.io.FilterOutputStream **/ + public void close() throws IOException { + finalizeStream(); + super.close(); + } + + +} + + diff --git a/src/main/java/org/apache/xmlgraphics/util/io/Base64DecodeStream.java b/src/main/java/org/apache/xmlgraphics/util/io/Base64DecodeStream.java new file mode 100644 index 0000000..496e098 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/util/io/Base64DecodeStream.java @@ -0,0 +1,196 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: Base64DecodeStream.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.util.io; + +import java.io.IOException; +import java.io.InputStream; + +// CSOFF: ConstantName +// CSOFF: MemberName +// CSOFF: MultipleVariableDeclarations +// CSOFF: NeedBraces +// CSOFF: OperatorWrap +// CSOFF: WhitespaceAround + +/** + * This class implements a Base64 Character decoder as specified in RFC1113. + * Unlike some other encoding schemes there is nothing in this encoding that + * tells the decoder where a buffer starts or stops, so to use it you will need + * to isolate your encoded data into a single chunk and then feed them + * this decoder. The simplest way to do that is to read all of the encoded + * data into a string and then use: + *

    + *      byte    data[];
    + *      InputStream is = new ByteArrayInputStream(data);
    + *      is = new Base64DecodeStream(is);
    + * 
    + * + * On errors, this class throws a IOException with the following detail + * strings: + *
    + *    "Base64DecodeStream: Bad Padding byte (2)."
    + *    "Base64DecodeStream: Bad Padding byte (1)."
    + * 
    + * + * @version $Id: Base64DecodeStream.java 1732018 2016-02-24 04:51:06Z gadams $ + * + * Originally authored by Thomas DeWeese, Vincent Hardy, and Chuck McManis. + */ + +public class Base64DecodeStream extends InputStream { + + InputStream src; + + public Base64DecodeStream(InputStream src) { + this.src = src; + } + + private static final byte[] PEM_ARRAY = new byte[256]; + static { + for (int i = 0; i < PEM_ARRAY.length; i++) { + PEM_ARRAY[i] = -1; + } + + int idx = 0; + for (char c = 'A'; c <= 'Z'; c++) { + PEM_ARRAY[c] = (byte)idx++; + } + for (char c = 'a'; c <= 'z'; c++) { + PEM_ARRAY[c] = (byte)idx++; + } + + for (char c = '0'; c <= '9'; c++) { + PEM_ARRAY[c] = (byte)idx++; + } + + PEM_ARRAY['+'] = (byte)idx++; + PEM_ARRAY['/'] = (byte)idx++; + } + + public boolean markSupported() { + return false; + } + + public void close() + throws IOException { + eof = true; + } + + public int available() + throws IOException { + return 3 - outOffset; + } + + byte[] decodeBuffer = new byte[4]; + byte[] outBuffer = new byte[3]; + int outOffset = 3; + boolean eof; + + public int read() throws IOException { + + if (outOffset == 3) { + if (eof || getNextAtom()) { + eof = true; + return -1; + } + } + + return ((int)outBuffer[outOffset++]) & 0xFF; + } + + public int read(byte []out, int offset, int len) + throws IOException { + + int idx = 0; + while (idx < len) { + if (outOffset == 3) { + if (eof || getNextAtom()) { + eof = true; + if (idx == 0) { + return -1; + } else { + return idx; + } + } + } + + out[offset + idx] = outBuffer[outOffset++]; + + idx++; + } + return idx; + } + + final boolean getNextAtom() throws IOException { + int count; + int a; + int b; + int c; + int d; + + int off = 0; + while (off != 4) { + count = src.read(decodeBuffer, off, 4 - off); + if (count == -1) { + return true; + } + + int in = off; + int out = off; + while (in < off + count) { + if ((decodeBuffer[in] != '\n') + && (decodeBuffer[in] != '\r') + && (decodeBuffer[in] != ' ')) { + decodeBuffer[out++] = decodeBuffer[in]; + } + in++; + } + + off = out; + } + + a = PEM_ARRAY[((int)decodeBuffer[0]) & 0xFF]; + b = PEM_ARRAY[((int)decodeBuffer[1]) & 0xFF]; + c = PEM_ARRAY[((int)decodeBuffer[2]) & 0xFF]; + d = PEM_ARRAY[((int)decodeBuffer[3]) & 0xFF]; + + outBuffer[0] = (byte)((a << 2) | (b >>> 4)); + outBuffer[1] = (byte)((b << 4) | (c >>> 2)); + outBuffer[2] = (byte)((c << 6) | d); + + if (decodeBuffer[3] != '=') { + // All three bytes are good. + outOffset = 0; + } else if (decodeBuffer[2] == '=') { + // Only one byte of output. + outBuffer[2] = outBuffer[0]; + outOffset = 2; + eof = true; + } else { + // Only two bytes of output. + outBuffer[2] = outBuffer[1]; + outBuffer[1] = outBuffer[0]; + outOffset = 1; + eof = true; + } + + return false; + } +} diff --git a/src/main/java/org/apache/xmlgraphics/util/io/Base64EncodeStream.java b/src/main/java/org/apache/xmlgraphics/util/io/Base64EncodeStream.java new file mode 100644 index 0000000..07da90a --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/util/io/Base64EncodeStream.java @@ -0,0 +1,247 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: Base64EncodeStream.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.util.io; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; +import java.io.UnsupportedEncodingException; + +// CSOFF: ConstantName +// CSOFF: InnerAssignment +// CSOFF: MultipleVariableDeclarations +// CSOFF: NeedBraces +// CSOFF: OneStatementPerLine +// CSOFF: WhitespaceAfter +// CSOFF: WhitespaceAround + +/** + * This class implements a Base64 Character encoder as specified in RFC1113. + * Unlike some other encoding schemes there is nothing in this encoding + * that indicates where a buffer starts or ends. + * + * This means that the encoded text will simply start with the first line + * of encoded text and end with the last line of encoded text. + * + * @version $Id: Base64EncodeStream.java 1732018 2016-02-24 04:51:06Z gadams $ + * + * Originally authored by Thomas DeWeese, Vincent Hardy, and Chuck McManis. + */ +public class Base64EncodeStream extends OutputStream { + + /** This array maps the 6 bit values to their characters */ + private static final byte[] PEM_ARRAY = { + // 0 1 2 3 4 5 6 7 + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', // 0 + 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', // 1 + 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', // 2 + 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', // 3 + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', // 4 + 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', // 5 + 'w', 'x', 'y', 'z', '0', '1', '2', '3', // 6 + '4', '5', '6', '7', '8', '9', '+', '/' // 7 + }; + + byte [] atom = new byte[3]; + int atomLen; + byte [] encodeBuf = new byte[4]; + int lineLen; + + PrintStream out; + boolean closeOutOnClose; + + public Base64EncodeStream(OutputStream out) { + try { + this.out = new PrintStream(out, false, "UTF-8"); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + closeOutOnClose = true; + } + + public Base64EncodeStream(OutputStream out, boolean closeOutOnClose) { + try { + this.out = new PrintStream(out, false, "UTF-8"); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + this.closeOutOnClose = closeOutOnClose; + } + + public void close() throws IOException { + if (out != null) { + encodeAtom(); + out.flush(); + if (closeOutOnClose) { + out.close(); + } + out = null; + } + } + + /** + * This can't really flush out output since that may generate + * '=' chars which would indicate the end of the stream. + * Instead we flush out. You can only be sure all output is + * writen by closing this stream. + */ + public void flush() throws IOException { + out.flush(); + } + + public void write(int b) throws IOException { + atom[atomLen++] = (byte)b; + if (atomLen == 3) { + encodeAtom(); + } + } + + public void write(byte []data) throws IOException { + encodeFromArray(data, 0, data.length); + } + + public void write(byte [] data, int off, int len) throws IOException { + encodeFromArray(data, off, len); + } + + /** + * enocodeAtom - Take three bytes of input and encode it as 4 + * printable characters. Note that if the length in len is less + * than three is encodes either one or two '=' signs to indicate + * padding characters. + */ + void encodeAtom() throws IOException { + byte a; + byte b; + byte c; + + switch (atomLen) { + case 0: return; + case 1: + a = atom[0]; + encodeBuf[0] = PEM_ARRAY[((a >>> 2) & 0x3F)]; + encodeBuf[1] = PEM_ARRAY[((a << 4) & 0x30)]; + encodeBuf[2] = encodeBuf[3] = '='; + break; + case 2: + a = atom[0]; + b = atom[1]; + encodeBuf[0] = PEM_ARRAY[((a >>> 2) & 0x3F)]; + encodeBuf[1] = PEM_ARRAY[(((a << 4) & 0x30) | ((b >>> 4) & 0x0F))]; + encodeBuf[2] = PEM_ARRAY[((b << 2) & 0x3C)]; + encodeBuf[3] = '='; + break; + default: + a = atom[0]; + b = atom[1]; + c = atom[2]; + encodeBuf[0] = PEM_ARRAY[((a >>> 2) & 0x3F)]; + encodeBuf[1] = PEM_ARRAY[(((a << 4) & 0x30) | ((b >>> 4) & 0x0F))]; + encodeBuf[2] = PEM_ARRAY[(((b << 2) & 0x3C) | ((c >>> 6) & 0x03))]; + encodeBuf[3] = PEM_ARRAY[c & 0x3F]; + } + if (lineLen == 64) { + out.println(); + lineLen = 0; + } + out.write(encodeBuf); + + lineLen += 4; + atomLen = 0; + } + + /** + * enocodeAtom - Take three bytes of input and encode it as 4 + * printable characters. Note that if the length in len is less + * than three is encodes either one or two '=' signs to indicate + * padding characters. + */ + void encodeFromArray(byte[] data, int offset, int len) + throws IOException { + byte a; + byte b; + byte c; + if (len == 0) { + return; + } + + // System.out.println("atomLen: " + atomLen + + // " len: " + len + + // " offset: " + offset); + + if (atomLen != 0) { + switch(atomLen) { + case 1: + atom[1] = data[offset++]; + len--; + atomLen++; + if (len == 0) { + return; + } + atom[2] = data[offset++]; + len--; + atomLen++; + break; + case 2: + atom[2] = data[offset++]; + len--; + atomLen++; + break; + default: + } + encodeAtom(); + } + + while (len >= 3) { + a = data[offset++]; + b = data[offset++]; + c = data[offset++]; + + encodeBuf[0] = PEM_ARRAY[((a >>> 2) & 0x3F)]; + encodeBuf[1] = PEM_ARRAY[(((a << 4) & 0x30) | ((b >>> 4) & 0x0F))]; + encodeBuf[2] = PEM_ARRAY[(((b << 2) & 0x3C) | ((c >>> 6) & 0x03))]; + encodeBuf[3] = PEM_ARRAY[c & 0x3F]; + out.write(encodeBuf); + + lineLen += 4; + if (lineLen == 64) { + out.println(); + lineLen = 0; + } + + len -= 3; + } + + switch (len) { + case 1: + atom[0] = data[offset]; + break; + case 2: + atom[0] = data[offset]; + atom[1] = data[offset + 1]; + break; + default: + } + atomLen = len; + } + + + +} diff --git a/src/main/java/org/apache/xmlgraphics/util/io/Finalizable.java b/src/main/java/org/apache/xmlgraphics/util/io/Finalizable.java new file mode 100644 index 0000000..f868bb1 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/util/io/Finalizable.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: Finalizable.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.util.io; + +/** + * This interface is used for special FilteredOutputStream classes that won't + * be closed (since this causes the target OutputStream to be closed, too) but + * where flush() is not enough, for example because a final marker has to be + * written to the target stream. + * + * @version $Id: Finalizable.java 1732018 2016-02-24 04:51:06Z gadams $ + */ +public interface Finalizable { + + /** + * This method can be called instead of close() on a subclass of + * FilteredOutputStream when a final marker has to be written to the target + * stream, but close() cannot be called. + * + * @exception java.io.IOException In case of an IO problem + */ + void finalizeStream() + throws java.io.IOException; + +} diff --git a/src/main/java/org/apache/xmlgraphics/util/io/FlateEncodeOutputStream.java b/src/main/java/org/apache/xmlgraphics/util/io/FlateEncodeOutputStream.java new file mode 100644 index 0000000..db19386 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/util/io/FlateEncodeOutputStream.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: FlateEncodeOutputStream.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.util.io; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * This class applies a FlateEncode filter to the stream. It is basically the + * normal DeflaterOutputStream except now also implementing the Finalizable + * interface. + * + * @version $Id: FlateEncodeOutputStream.java 1732018 2016-02-24 04:51:06Z gadams $ + */ +public class FlateEncodeOutputStream extends java.util.zip.DeflaterOutputStream + implements Finalizable { + + /** @see java.util.zip.DeflaterOutputStream **/ + public FlateEncodeOutputStream(OutputStream out) { + super(out); + } + + /** @see Finalizable **/ + public void finalizeStream() throws IOException { + finish(); + flush(); + + // ensure that Deflater resources are released + def.end(); + + if (out instanceof Finalizable) { + ((Finalizable)out).finalizeStream(); + } + } + +} + + diff --git a/src/main/java/org/apache/xmlgraphics/util/io/RunLengthEncodeOutputStream.java b/src/main/java/org/apache/xmlgraphics/util/io/RunLengthEncodeOutputStream.java new file mode 100644 index 0000000..dae90ed --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/util/io/RunLengthEncodeOutputStream.java @@ -0,0 +1,189 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: RunLengthEncodeOutputStream.java 1804124 2017-08-04 14:13:54Z ssteiner $ */ + +package org.apache.xmlgraphics.util.io; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; + + +/** + * This class applies a RunLengthEncode filter to the stream. + * + * @version $Id: RunLengthEncodeOutputStream.java 1804124 2017-08-04 14:13:54Z ssteiner $ + * + * Originally authored by Stephen Wolke. + */ +public class RunLengthEncodeOutputStream extends FilterOutputStream + implements Finalizable { + + private static final int MAX_SEQUENCE_COUNT = 127; + private static final int END_OF_DATA = 128; + private static final int BYTE_MAX = 256; + + private static final int NOT_IDENTIFY_SEQUENCE = 0; + private static final int START_SEQUENCE = 1; + private static final int IN_SEQUENCE = 2; + private static final int NOT_IN_SEQUENCE = 3; + + private int runCount; + private int isSequence = NOT_IDENTIFY_SEQUENCE; + private byte[] runBuffer = new byte[MAX_SEQUENCE_COUNT + 1]; + + + /** @see java.io.FilterOutputStream **/ + public RunLengthEncodeOutputStream(OutputStream out) { + super(out); + } + + + /** @see java.io.FilterOutputStream **/ + public void write(byte b) + throws java.io.IOException { + runBuffer[runCount] = b; + + switch (runCount) { + case 0: + runCount = 0; + isSequence = NOT_IDENTIFY_SEQUENCE; + runCount++; + break; + case 1: + if (runBuffer[runCount] != runBuffer[runCount - 1]) { + isSequence = NOT_IN_SEQUENCE; + } + runCount++; + break; + case 2: + if (runBuffer[runCount] != runBuffer[runCount - 1]) { + isSequence = NOT_IN_SEQUENCE; + } else { + if (isSequence == NOT_IN_SEQUENCE) { + isSequence = START_SEQUENCE; + } else { + isSequence = IN_SEQUENCE; + } + } + runCount++; + break; + case MAX_SEQUENCE_COUNT: + if (isSequence == IN_SEQUENCE) { + out.write(BYTE_MAX - (MAX_SEQUENCE_COUNT - 1)); + out.write(runBuffer[runCount - 1]); + runBuffer[0] = runBuffer[runCount]; + runCount = 1; + } else { + out.write(MAX_SEQUENCE_COUNT); + out.write(runBuffer, 0, runCount + 1); + runCount = 0; + } + isSequence = NOT_IDENTIFY_SEQUENCE; + break; + default: + switch (isSequence) { + case NOT_IN_SEQUENCE: + if (runBuffer[runCount] == runBuffer[runCount - 1]) { + isSequence = START_SEQUENCE; + } + runCount++; + break; + case START_SEQUENCE: + if (runBuffer[runCount] == runBuffer[runCount - 1]) { + out.write(runCount - 3); + out.write(runBuffer, 0, runCount - 2); + runBuffer[0] = runBuffer[runCount]; + runBuffer[1] = runBuffer[runCount]; + runBuffer[2] = runBuffer[runCount]; + runCount = 3; + isSequence = IN_SEQUENCE; + break; + } else { + isSequence = NOT_IN_SEQUENCE; + runCount++; + break; + } + case IN_SEQUENCE: + default: + if (runBuffer[runCount] != runBuffer[runCount - 1]) { + out.write(BYTE_MAX - (runCount - 1)); + out.write(runBuffer[runCount - 1]); + runBuffer[0] = runBuffer[runCount]; + runCount = 1; + isSequence = NOT_IDENTIFY_SEQUENCE; + break; + } + runCount++; + break; + } + } + } + + + /** @see java.io.FilterOutputStream **/ + public void write(byte[] b) + throws IOException { + + for (byte aB : b) { + this.write(aB); + } + } + + + /** @see java.io.FilterOutputStream **/ + public void write(byte[] b, int off, int len) + throws IOException { + + for (int i = 0; i < len; i++) { + this.write(b[off + i]); + } + } + + + /** @see Finalizable **/ + public void finalizeStream() + throws IOException { + switch (isSequence) { + case IN_SEQUENCE: + out.write(BYTE_MAX - (runCount - 1)); + out.write(runBuffer[runCount - 1]); + break; + default: + out.write(runCount - 1); + out.write(runBuffer, 0, runCount); + } + + out.write(END_OF_DATA); + + flush(); + if (out instanceof Finalizable) { + ((Finalizable) out).finalizeStream(); + } + } + + + /** @see java.io.FilterOutputStream **/ + public void close() + throws IOException { + finalizeStream(); + super.close(); + } + +} + diff --git a/src/main/java/org/apache/xmlgraphics/util/io/SubInputStream.java b/src/main/java/org/apache/xmlgraphics/util/io/SubInputStream.java new file mode 100644 index 0000000..8d7c913 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/util/io/SubInputStream.java @@ -0,0 +1,114 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: SubInputStream.java 1610846 2014-07-15 20:44:18Z vhennebert $ */ + +package org.apache.xmlgraphics.util.io; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * This class is a FilterInputStream descendant that reads from an underlying InputStream + * up to a defined number of bytes or the end of the underlying stream. Closing this InputStream + * will not result in the underlying InputStream to be closed, too. + *

    + * This InputStream can be used to read chunks from a larger file of which the length is + * known in advance. + */ +public class SubInputStream extends FilterInputStream { + + /** Indicates the number of bytes remaining to be read from the underlying InputStream. */ + private long bytesToRead; + + /** + * Indicates whether the underlying stream should be closed when the {@link #close()} method + * is called. + */ + private boolean closeUnderlying; + + /** + * Creates a new SubInputStream. + * @param in the InputStream to read from + * @param maxLen the maximum number of bytes to read from the underlying InputStream until + * the end-of-file is signalled. + * @param closeUnderlying true if the underlying stream should be closed when the + * {@link #close()} method is called. + */ + public SubInputStream(InputStream in, long maxLen, boolean closeUnderlying) { + super(in); + this.bytesToRead = maxLen; + this.closeUnderlying = closeUnderlying; + } + + /** + * Creates a new SubInputStream. The underlying stream is not closed, when close() is called. + * @param in the InputStream to read from + * @param maxLen the maximum number of bytes to read from the underlying InputStream until + * the end-of-file is signalled. + */ + public SubInputStream(InputStream in, long maxLen) { + this(in, maxLen, false); + } + + /** {@inheritDoc} */ + public int read() throws IOException { + if (bytesToRead > 0) { + int result = super.read(); + if (result >= 0) { + bytesToRead--; + return result; + } else { + return -1; + } + } else { + return -1; + } + } + + /** {@inheritDoc} */ + public int read(byte[] b, int off, int len) throws IOException { + if (bytesToRead == 0) { + return -1; + } + int effRead = (int)Math.min(bytesToRead, len); + //cast to int is safe because len can never be bigger than Integer.MAX_VALUE + + int result = super.read(b, off, effRead); + if (result >= 0) { + bytesToRead -= result; + } + return result; + } + + /** {@inheritDoc} */ + public long skip(long n) throws IOException { + long effRead = Math.min(bytesToRead, n); + long result = super.skip(effRead); + bytesToRead -= result; + return result; + } + + /** {@inheritDoc} */ + public void close() throws IOException { + this.bytesToRead = 0; + if (this.closeUnderlying) { + super.close(); + } + } +} diff --git a/src/main/java/org/apache/xmlgraphics/util/io/package.html b/src/main/java/org/apache/xmlgraphics/util/io/package.html new file mode 100644 index 0000000..9ba071e --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/util/io/package.html @@ -0,0 +1,23 @@ + + + +org.apache.xmlgraphics.util.io Package + +

    I/O-related classes (encoders/decoders, enhanced compressors etc.).

    + + \ No newline at end of file diff --git a/src/main/java/org/apache/xmlgraphics/util/package.html b/src/main/java/org/apache/xmlgraphics/util/package.html new file mode 100644 index 0000000..bce5d9a --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/util/package.html @@ -0,0 +1,23 @@ + + + +org.apache.xmlgraphics.util Package + +

    Various utilities used by the Apache XML Graphics project.

    + + \ No newline at end of file diff --git a/src/main/java/org/apache/xmlgraphics/util/uri/CommonURIResolver.java b/src/main/java/org/apache/xmlgraphics/util/uri/CommonURIResolver.java new file mode 100644 index 0000000..75d2635 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/util/uri/CommonURIResolver.java @@ -0,0 +1,112 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: CommonURIResolver.java 1804124 2017-08-04 14:13:54Z ssteiner $ */ + +package org.apache.xmlgraphics.util.uri; + +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +import javax.xml.transform.Source; +import javax.xml.transform.TransformerException; +import javax.xml.transform.URIResolver; + +import org.apache.xmlgraphics.util.Service; + +/** + * A URI Resolver which supports pluggable entities via the {@link Service} + * mechanism. + *

    + * This resolver will try all resolvers registered as an {@link URIResolver} + * class. For proper operation, the registers URIResolvers must return null if + * they cannot handle the given URI and fail fast. + */ +public class CommonURIResolver implements URIResolver { + + private final List uriResolvers = new LinkedList(); + + private static final class DefaultInstanceHolder { + private static final CommonURIResolver INSTANCE = new CommonURIResolver(); + } + + /** + * Creates a new CommonURIResolver. Use this if you need support for + * resolvers in the current context. + * @see CommonURIResolver#getDefaultURIResolver() + */ + public CommonURIResolver() { + Iterator iter = Service.providers(URIResolver.class); + while (iter.hasNext()) { + URIResolver resolver = (URIResolver) iter.next(); + register(resolver); + } + } + + /** + * Retrieve the default resolver instance. + * + * @return the default resolver instance. + */ + public static CommonURIResolver getDefaultURIResolver() { + return DefaultInstanceHolder.INSTANCE; + } + + /** {@inheritDoc} */ + public Source resolve(String href, String base) { + synchronized (uriResolvers) { + for (Object uriResolver : uriResolvers) { + final URIResolver currentResolver = (URIResolver) uriResolver; + try { + final Source result = currentResolver.resolve(href, base); + if (result != null) { + return result; + } + } catch (TransformerException e) { + // Ignore. + } + } + } + return null; + } + + /** + * Register a given {@link URIResolver} while the software is running. + * + * @param uriResolver + * the resolver to register. + */ + public void register(URIResolver uriResolver) { + synchronized (uriResolvers) { + uriResolvers.add(uriResolver); + } + } + + /** + * Unregister a given {@link URIResolver} while the software is running. + * + * @param uriResolver + * the resolver to unregister. + */ + public void unregister(URIResolver uriResolver) { + synchronized (uriResolvers) { + uriResolvers.remove(uriResolver); + } + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/util/uri/DataURIResolver.java b/src/main/java/org/apache/xmlgraphics/util/uri/DataURIResolver.java new file mode 100644 index 0000000..1a53c8b --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/util/uri/DataURIResolver.java @@ -0,0 +1,101 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DataURIResolver.java 1681108 2015-05-22 13:26:12Z ssteiner $ */ + +package org.apache.xmlgraphics.util.uri; + +import java.io.ByteArrayInputStream; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; + +import javax.xml.transform.Source; +import javax.xml.transform.TransformerException; +import javax.xml.transform.URIResolver; +import javax.xml.transform.stream.StreamSource; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.apache.xmlgraphics.util.io.Base64DecodeStream; + +/** + * Resolves data URLs (described in RFC 2397) returning its data as a StreamSource. + * + * @see javax.xml.transform.URIResolver + * @see RFC 2397 + */ +public class DataURIResolver implements URIResolver { + + /** logger */ + private static final Log LOG = LogFactory.getLog(URIResolver.class); + + + /** + * {@inheritDoc} + */ + public Source resolve(String href, String base) throws TransformerException { + if (href.startsWith("data:")) { + return parseDataURI(href); + } else { + return null; + } + } + + /** + * Parses inline data URIs as generated by MS Word's XML export and FO + * stylesheet. + * + * @see RFC 2397 + */ + private Source parseDataURI(String href) { + int commaPos = href.indexOf(','); + // header is of the form data:[][;base64] + String header = href.substring(0, commaPos); + String data = href.substring(commaPos + 1); + if (header.endsWith(";base64")) { + byte[] bytes = new byte[0]; + try { + bytes = data.getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + ByteArrayInputStream encodedStream = new ByteArrayInputStream(bytes); + Base64DecodeStream decodedStream = new Base64DecodeStream( + encodedStream); + return new StreamSource(decodedStream, href); + } else { + String encoding = "UTF-8"; + final int charsetpos = header.indexOf(";charset="); + if (charsetpos > 0) { + encoding = header.substring(charsetpos + 9); + } + try { + final String unescapedString = URLDecoder + .decode(data, encoding); + return new StreamSource(new java.io.StringReader( + unescapedString), href); + } catch (IllegalArgumentException e) { + LOG.warn(e.getMessage()); + } catch (UnsupportedEncodingException e) { + LOG.warn(e.getMessage()); + } + } + return null; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/util/uri/DataURLUtil.java b/src/main/java/org/apache/xmlgraphics/util/uri/DataURLUtil.java new file mode 100644 index 0000000..59ba989 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/util/uri/DataURLUtil.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DataURLUtil.java 1345683 2012-06-03 14:50:33Z gadams $ */ + +package org.apache.xmlgraphics.util.uri; + +import java.io.IOException; +import java.io.InputStream; +import java.io.StringWriter; +import java.io.Writer; + +import org.apache.commons.io.IOUtils; + +import org.apache.xmlgraphics.util.WriterOutputStream; +import org.apache.xmlgraphics.util.io.Base64EncodeStream; + +/** + * Utility classes for generating RFC 2397 data URLs. + */ +public final class DataURLUtil { + + private DataURLUtil() { + } + + /** + * Creates a new data URL and returns it as a String. + * @param in the InputStream to read the data from + * @param mediatype the MIME type of the content, or null + * @return the newly created data URL + * @throws IOException if an I/O error occurs + */ + public static String createDataURL(InputStream in, String mediatype) throws IOException { + StringWriter writer = new StringWriter(); + writeDataURL(in, mediatype, writer); + return writer.toString(); + } + + /** + * Generates a data URL and writes it to a Writer. + * @param in the InputStream to read the data from + * @param mediatype the MIME type of the content, or null + * @param writer the Writer to write to + * @throws IOException if an I/O error occurs + */ + public static void writeDataURL(InputStream in, String mediatype, Writer writer) + throws IOException { + writer.write("data:"); + if (mediatype != null) { + writer.write(mediatype); + } + writer.write(";base64,"); + Base64EncodeStream out = new Base64EncodeStream( + new WriterOutputStream(writer, "US-ASCII"), false); + IOUtils.copy(in, out); + out.close(); + } +} diff --git a/src/main/java/org/apache/xmlgraphics/xmp/Metadata.java b/src/main/java/org/apache/xmlgraphics/xmp/Metadata.java new file mode 100644 index 0000000..499f6f2 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/xmp/Metadata.java @@ -0,0 +1,157 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: Metadata.java 1804124 2017-08-04 14:13:54Z ssteiner $ */ + +package org.apache.xmlgraphics.xmp; + +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.AttributesImpl; + +import org.apache.xmlgraphics.util.QName; +import org.apache.xmlgraphics.util.XMLizable; +import org.apache.xmlgraphics.xmp.merge.MergeRuleSet; +import org.apache.xmlgraphics.xmp.merge.PropertyMerger; + +/** + * This class represents the root of an XMP metadata tree. It's more or less equivalent to the + * x:xmpmeta element together with its nested rdf:RDF element. + */ +public class Metadata implements XMLizable, PropertyAccess { + + private Map properties = new java.util.HashMap(); + + /** {@inheritDoc} */ + public void setProperty(XMPProperty prop) { + properties.put(prop.getName(), prop); + } + + /** {@inheritDoc} */ + public XMPProperty getProperty(String uri, String localName) { + return getProperty(new QName(uri, localName)); + } + + /** {@inheritDoc} */ + public XMPProperty getProperty(QName name) { + XMPProperty prop = (XMPProperty)properties.get(name); + return prop; + } + + /** {@inheritDoc} */ + public XMPProperty removeProperty(QName name) { + return (XMPProperty)properties.remove(name); + } + + /** {@inheritDoc} */ + public XMPProperty getValueProperty() { + return getProperty(XMPConstants.RDF_VALUE); + } + + /** {@inheritDoc} */ + public int getPropertyCount() { + return this.properties.size(); + } + + /** {@inheritDoc} */ + public Iterator iterator() { + return this.properties.keySet().iterator(); + } + + /** + * Merges this metadata object into a given target metadata object. The merge rule set provided + * by each schema is used for the merge. + * @param target the target metadata to merge the local metadata into + */ + public void mergeInto(Metadata target, List exclude) { + XMPSchemaRegistry registry = XMPSchemaRegistry.getInstance(); + for (Object o : properties.values()) { + XMPProperty prop = (XMPProperty) o; + XMPSchema schema = registry.getSchema(prop.getNamespace()); + if (!exclude.contains(schema.getClass())) { + MergeRuleSet rules = schema.getDefaultMergeRuleSet(); + PropertyMerger merger = rules.getPropertyMergerFor(prop); + merger.merge(prop, target); + } + } + } + + /** {@inheritDoc} */ + public void toSAX(ContentHandler handler) throws SAXException { + AttributesImpl atts = new AttributesImpl(); + handler.startPrefixMapping("x", XMPConstants.XMP_NAMESPACE); + handler.startElement(XMPConstants.XMP_NAMESPACE, "xmpmeta", "x:xmpmeta", atts); + handler.startPrefixMapping("rdf", XMPConstants.RDF_NAMESPACE); + handler.startElement(XMPConstants.RDF_NAMESPACE, "RDF", "rdf:RDF", atts); + //Get all property namespaces + Set namespaces = new java.util.HashSet(); + Iterator iter = properties.keySet().iterator(); + while (iter.hasNext()) { + QName n = ((QName)iter.next()); + namespaces.add(n.getNamespaceURI()); + } + //One Description element per namespace + iter = namespaces.iterator(); + while (iter.hasNext()) { + String ns = (String)iter.next(); + XMPSchema schema = XMPSchemaRegistry.getInstance().getSchema(ns); + String prefix = (schema != null ? schema.getPreferredPrefix() : null); + + boolean first = true; + boolean empty = true; + + for (Object o : properties.values()) { + XMPProperty prop = (XMPProperty) o; + if (prop.getName().getNamespaceURI().equals(ns)) { + if (first) { + if (prefix == null) { + prefix = prop.getName().getPrefix(); + } + atts.clear(); + atts.addAttribute(XMPConstants.RDF_NAMESPACE, + "about", "rdf:about", "CDATA", ""); + if (prefix != null) { + handler.startPrefixMapping(prefix, ns); + } + handler.startElement(XMPConstants.RDF_NAMESPACE, + "Description", "rdf:Description", atts); + empty = false; + first = false; + } + prop.toSAX(handler); + } + } + if (!empty) { + handler.endElement(XMPConstants.RDF_NAMESPACE, "Description", "rdf:Description"); + if (prefix != null) { + handler.endPrefixMapping(prefix); + } + } + } + + handler.endElement(XMPConstants.RDF_NAMESPACE, "RDF", "rdf:RDF"); + handler.endPrefixMapping("rdf"); + handler.endElement(XMPConstants.XMP_NAMESPACE, "xmpmeta", "x:xmpmeta"); + handler.endPrefixMapping("x"); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/xmp/PropertyAccess.java b/src/main/java/org/apache/xmlgraphics/xmp/PropertyAccess.java new file mode 100644 index 0000000..1fd1980 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/xmp/PropertyAccess.java @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PropertyAccess.java 1345683 2012-06-03 14:50:33Z gadams $ */ + +package org.apache.xmlgraphics.xmp; + +import java.util.Iterator; + +import org.apache.xmlgraphics.util.QName; + +/** + * This interface is implemented by the top-level Metadata class and stuctured properties. + */ +public interface PropertyAccess { + + /** + * Sets a property. + * @param prop the property + */ + void setProperty(XMPProperty prop); + + /** + * Returns a property + * @param uri the namespace URI of the property + * @param localName the local name of the property + * @return the requested property or null if it's not available + */ + XMPProperty getProperty(String uri, String localName); + + /** + * Returns a property. + * @param name the name of the property + * @return the requested property or null if it's not available + */ + XMPProperty getProperty(QName name); + + /** + * Removes a property and returns it if it was found. + * @param name the name of the property + * @return the removed property or null if it was not found + */ + XMPProperty removeProperty(QName name); + + /** + * Returns the rdf:value property. This is a shortcut for getProperty(XMPConstants.RDF_VALUE). + * @return the rdf:value property or null if it's no available + */ + XMPProperty getValueProperty(); + + /** + * Returns the number of properties. + * @return the number of properties in this metadata object. + */ + int getPropertyCount(); + + /** + * Returns an Iterator over all properties in this structured property. + * @return an Iterator over all properties + */ + Iterator iterator(); + +} diff --git a/src/main/java/org/apache/xmlgraphics/xmp/XMPArray.java b/src/main/java/org/apache/xmlgraphics/xmp/XMPArray.java new file mode 100644 index 0000000..d00b2a5 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/xmp/XMPArray.java @@ -0,0 +1,250 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: XMPArray.java 1681108 2015-05-22 13:26:12Z ssteiner $ */ + +package org.apache.xmlgraphics.xmp; + +import java.net.URI; + +import java.util.List; + +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.AttributesImpl; + +/** + * Represents an XMP array as defined by the XMP specification. + */ +public class XMPArray extends XMPComplexValue { + //TODO Property qualifiers are currently not supported, yet. + + private XMPArrayType type; + private List values = new java.util.ArrayList(); + private List xmllang = new java.util.ArrayList(); + + /** + * Main constructor + * @param type the intended type of array + */ + public XMPArray(XMPArrayType type) { + this.type = type; + } + + /** @return the type of array */ + public XMPArrayType getType() { + return this.type; + } + + /** + * Returns the value at a given position. + * @param idx the index of the requested value + * @return the value at the given position + */ + public Object getValue(int idx) { + return this.values.get(idx); + } + + /** + * Returns the structure at a given position. If the value is not a structure a + * ClassCastException is thrown. + * @param idx the index of the requested value + * @return the structure at the given position + */ + public XMPStructure getStructure(int idx) { + return (XMPStructure)this.values.get(idx); + } + + /** {@inheritDoc} */ + public Object getSimpleValue() { + if (values.size() == 1) { + return getValue(0); + } else if (values.size() > 1) { + return getLangValue(XMPConstants.DEFAULT_LANGUAGE); + } else { + return null; + } + } + + private String getParentLanguage(String lang) { + if (lang == null) { + return null; + } + int pos = lang.indexOf('-'); + if (pos > 0) { + String parent = lang.substring(0, pos); + return parent; + } + return null; + } + + /** + * Returns a language-dependent values (available for alternative arrays). + * @param lang the language ("x-default" for the default value) + * @return the requested value + */ + public String getLangValue(String lang) { + String v = null; + String valueForParentLanguage = null; + for (int i = 0, c = values.size(); i < c; i++) { + String l = (String)xmllang.get(i); + if ((l == null && lang == null) || (l != null && l.equals(lang))) { + v = values.get(i).toString(); + break; + } + if (l != null && lang != null) { + //Check for "parent" language, too ("en" matches "en-GB") + String parent = getParentLanguage(l); + if (parent != null && parent.equals(lang)) { + valueForParentLanguage = values.get(i).toString(); + } + } + } + if (lang != null && v == null && valueForParentLanguage != null) { + //Use value found for parent language + v = valueForParentLanguage; + } + if (lang == null && v == null) { + v = getLangValue(XMPConstants.DEFAULT_LANGUAGE); + if (v == null && values.size() > 0) { + v = getValue(0).toString(); //get first + } + } + return v; + } + + /** + * Removes a language-dependent value. + * @param lang the language ("x-default" for the default value) + * @return the removed value (or null if no value was set) + */ + public String removeLangValue(String lang) { + if (lang == null || "".equals(lang)) { + lang = XMPConstants.DEFAULT_LANGUAGE; + } + for (int i = 0, c = values.size(); i < c; i++) { + String l = (String)xmllang.get(i); + if ((XMPConstants.DEFAULT_LANGUAGE.equals(lang) && l == null) || lang.equals(l)) { + String value = (String)values.remove(i); + xmllang.remove(i); + return value; + } + } + return null; + } + + /** + * Adds a new value to the array + * @param value the value + */ + public void add(Object value) { + values.add(value); + xmllang.add(null); + } + + /** + * Removes a value from the array. If the value doesn't exist, nothing happens. + * @param value the value to be removed + * @return true if the value was removed, false if it wasn't found + */ + public boolean remove(String value) { + int idx = values.indexOf(value); + if (idx >= 0) { + values.remove(idx); + xmllang.remove(idx); + return true; + } + return false; + + } + + /** + * Adds a language-dependent value to the array. Make sure not to add the same language twice. + * @param value the value + * @param lang the language ("x-default" for the default value) + */ + public void add(String value, String lang) { + values.add(value); + xmllang.add(lang); + } + + /** + * Returns the current number of values in the array. + * @return the current number of values in the array + */ + public int getSize() { + return this.values.size(); + } + + /** + * Indicates whether the array is empty or not. + * @return true if the array is empty + */ + public boolean isEmpty() { + return getSize() == 0; + } + + /** + * Converts the array to an object array. + * @return an object array of all values in the array + */ + public Object[] toObjectArray() { + Object[] res = new Object[getSize()]; + for (int i = 0, c = res.length; i < c; i++) { + res[i] = getValue(i); + } + return res; + } + + /** {@inheritDoc} */ + public void toSAX(ContentHandler handler) throws SAXException { + AttributesImpl atts = new AttributesImpl(); + handler.startElement(XMPConstants.RDF_NAMESPACE, + type.getName(), "rdf:" + type.getName(), atts); + for (int i = 0, c = values.size(); i < c; i++) { + String lang = (String)xmllang.get(i); + atts.clear(); + Object v = values.get(i); + if (lang != null) { + atts.addAttribute(XMPConstants.XML_NS, "lang", "xml:lang", "CDATA", lang); + } + if (v instanceof URI) { + atts.addAttribute(XMPConstants.RDF_NAMESPACE, "resource", + "rdf:resource", "CDATA", ((URI)v).toString()); + } + handler.startElement(XMPConstants.RDF_NAMESPACE, + "li", "rdf:li", atts); + if (v instanceof XMPComplexValue) { + ((XMPComplexValue)v).toSAX(handler); + } else if (!(v instanceof URI)) { + String value = (String)values.get(i); + char[] chars = value.toCharArray(); + handler.characters(chars, 0, chars.length); + } + handler.endElement(XMPConstants.RDF_NAMESPACE, + "li", "rdf:li"); + } + handler.endElement(XMPConstants.RDF_NAMESPACE, + type.getName(), "rdf:" + type.getName()); + } + + /** {@inheritDoc} */ + public String toString() { + return "XMP array: " + type + ", " + getSize(); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/xmp/XMPArrayType.java b/src/main/java/org/apache/xmlgraphics/xmp/XMPArrayType.java new file mode 100644 index 0000000..79a7a08 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/xmp/XMPArrayType.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: XMPArrayType.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.xmp; + +/** Enum class for XMP array types. */ +public final class XMPArrayType { + + /** the unordered array */ + public static final XMPArrayType BAG = new XMPArrayType("Bag"); + /** the ordered array */ + public static final XMPArrayType SEQ = new XMPArrayType("Seq"); + /** the alternative array */ + public static final XMPArrayType ALT = new XMPArrayType("Alt"); + + private String name; + + /** + * Constructor to add a new named item. + * @param name Name of the item. + */ + private XMPArrayType(String name) { + this.name = name; + } + + /** @return the name of the enum */ + public String getName() { + return this.name; + } + + /** @see java.lang.Object#toString() */ + public String toString() { + return "rdf:" + name; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/xmp/XMPComplexValue.java b/src/main/java/org/apache/xmlgraphics/xmp/XMPComplexValue.java new file mode 100644 index 0000000..210a1d1 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/xmp/XMPComplexValue.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: XMPComplexValue.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.xmp; + +import org.apache.xmlgraphics.util.XMLizable; + +/** + * Base class for complex data types in XMP. + */ +public abstract class XMPComplexValue implements XMLizable { + + /** + * Returns a normal Java object representing the value if it is available. + * @return a simple object value or null if no such value can be returned (for example, + * because the value is an array and has multiple entries. + */ + public abstract Object getSimpleValue(); + +} diff --git a/src/main/java/org/apache/xmlgraphics/xmp/XMPConstants.java b/src/main/java/org/apache/xmlgraphics/xmp/XMPConstants.java new file mode 100644 index 0000000..8409e35 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/xmp/XMPConstants.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: XMPConstants.java 1685768 2015-06-16 11:46:06Z ssteiner $ */ + +package org.apache.xmlgraphics.xmp; + +import org.apache.xmlgraphics.util.QName; + +/** + * Constants used in XMP metadata. + */ +public interface XMPConstants { + + /** Namespace URI for the xml: prefix */ + String XML_NS = "http://www.w3.org/XML/1998/namespace"; + + /** Namespace URI for the xmlns: prefix */ + String XMLNS_NAMESPACE = "http://www.w3.org/2000/xmlns/"; + + /** Namespace URI for XMP */ + String XMP_NAMESPACE = "adobe:ns:meta/"; + + /** Namespace URI for RDF */ + String RDF_NAMESPACE = "http://www.w3.org/1999/02/22-rdf-syntax-ns#"; + + /** Namespace URI for Dublin Core */ + String DUBLIN_CORE_NAMESPACE = "http://purl.org/dc/elements/1.1/"; + + /** Namespace URI for the XMP Basic Schema */ + String XMP_BASIC_NAMESPACE = "http://ns.adobe.com/xap/1.0/"; + + /** Namespace URI for the Adobe PDF Schema */ + String ADOBE_PDF_NAMESPACE = "http://ns.adobe.com/pdf/1.3/"; + + /** Namespace URI for PDF X */ + String PDF_X_IDENTIFICATION = "http://www.npes.org/pdfx/ns/id/"; + + /** Namespace URI for PDF VT */ + String PDF_VT_IDENTIFICATION = "http://www.npes.org/pdfvt/ns/id/"; + + String PDF_UA_IDENTIFICATION = "http://www.aiim.org/pdfua/ns/id/"; + + /** Namespace URI for XMP Media Management */ + String XAP_MM_NAMESPACE = "http://ns.adobe.com/xap/1.0/mm/"; + + /** + * Namespace URI for the PDF/A Identification Schema + * (from the technical corrigendum 1 of ISO 19005-1:2005, note that the trailing slash + * was missing in the original ISO 19005-1:2005 specification) + */ + String PDF_A_IDENTIFICATION = "http://www.aiim.org/pdfa/ns/id/"; + + /** Default language for the xml:lang property */ + String DEFAULT_LANGUAGE = "x-default"; + + /** QName for rdf:value */ + QName RDF_VALUE = new QName(RDF_NAMESPACE, "rdf", "value"); + +} diff --git a/src/main/java/org/apache/xmlgraphics/xmp/XMPHandler.java b/src/main/java/org/apache/xmlgraphics/xmp/XMPHandler.java new file mode 100644 index 0000000..dfbb543 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/xmp/XMPHandler.java @@ -0,0 +1,309 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: XMPHandler.java 1681108 2015-05-22 13:26:12Z ssteiner $ */ + +package org.apache.xmlgraphics.xmp; + +import java.net.URI; +import java.net.URISyntaxException; + +import java.util.Stack; + +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.AttributesImpl; +import org.xml.sax.helpers.DefaultHandler; + +import org.apache.xmlgraphics.util.QName; + +/** + * Passive XMP parser implemented as a SAX DefaultHandler. After the XML document has been parsed + * the Metadata object can be retrieved. + */ +public class XMPHandler extends DefaultHandler { + + private Metadata meta; + + private StringBuffer content = new StringBuffer(); + private Stack attributesStack = new Stack(); + private Stack nestingInfoStack = new Stack(); + private Stack contextStack = new Stack(); + + /** @return the parsed metadata, available after the parsing. */ + public Metadata getMetadata() { + return this.meta; + } + + private boolean hasComplexContent() { + Object obj = this.contextStack.peek(); + return !(obj instanceof QName); + } + + private PropertyAccess getCurrentProperties() { + Object obj = this.contextStack.peek(); + if (obj instanceof PropertyAccess) { + return (PropertyAccess)obj; + } else { + return null; + } + } + + private QName getCurrentPropName() { + Object obj = this.contextStack.peek(); + if (obj instanceof QName) { + return (QName)obj; + } else { + return null; + } + } + + private QName popCurrentPropName() throws SAXException { + Object obj = this.contextStack.pop(); + this.nestingInfoStack.pop(); + if (obj instanceof QName) { + return (QName)obj; + } else { + throw new SAXException("Invalid XMP structure. Property name expected"); + } + } + +// private XMPComplexValue getCurrentComplexValue() { +// Object obj = this.contextStack.peek(); +// if (obj instanceof XMPComplexValue) { +// return (XMPComplexValue)obj; +// } else { +// return null; +// } +// } + + private XMPStructure getCurrentStructure() { + Object obj = this.contextStack.peek(); + if (obj instanceof XMPStructure) { + return (XMPStructure)obj; + } else { + return null; + } + } + + private XMPArray getCurrentArray(boolean required) throws SAXException { + Object obj = this.contextStack.peek(); + if (obj instanceof XMPArray) { + return (XMPArray)obj; + } else { + if (required) { + throw new SAXException("Invalid XMP structure. Not in array"); + } else { + return null; + } + } + } + + // --- Overrides --- + + /** {@inheritDoc} */ + public void startElement(String uri, String localName, String qName, Attributes attributes) + throws SAXException { + super.startElement(uri, localName, qName, attributes); + content.setLength(0); //Reset text buffer (see characters()) + attributesStack.push(new AttributesImpl(attributes)); + + if (XMPConstants.XMP_NAMESPACE.equals(uri)) { + if (!"xmpmeta".equals(localName)) { + throw new SAXException("Expected x:xmpmeta element, not " + qName); + } + if (this.meta != null) { + throw new SAXException("Invalid XMP document. Root already received earlier."); + } + this.meta = new Metadata(); + this.contextStack.push(this.meta); + this.nestingInfoStack.push("metadata"); + } else if (XMPConstants.RDF_NAMESPACE.equals(uri)) { + if ("RDF".equals(localName)) { + if (this.meta == null) { + this.meta = new Metadata(); + this.contextStack.push(this.meta); + this.nestingInfoStack.push("metadata"); + } + } else if ("Description".equals(localName)) { + String about = attributes.getValue(XMPConstants.RDF_NAMESPACE, "about"); + for (int i = 0, c = attributes.getLength(); i < c; i++) { + String ns = attributes.getURI(i); + if (XMPConstants.RDF_NAMESPACE.equals(ns)) { + //ignore + } else if (XMPConstants.XMLNS_NAMESPACE.equals(ns)) { + //ignore + } else if ("".equals(ns)) { + //ignore + } else { + String qn = attributes.getQName(i); + String v = attributes.getValue(i); + XMPProperty prop = new XMPProperty(new QName(ns, qn), v); + getCurrentProperties().setProperty(prop); + } + } + if (this.contextStack.peek().equals(this.meta)) { + //rdf:RDF is the parent + } else { + if (about != null) { + throw new SAXException( + "Nested rdf:Description elements may not have an about property"); + } + startStructure(); + } + } else if ("Seq".equals(localName)) { + XMPArray array = new XMPArray(XMPArrayType.SEQ); + this.contextStack.push(array); + this.nestingInfoStack.push("Seq"); + } else if ("Bag".equals(localName)) { + XMPArray array = new XMPArray(XMPArrayType.BAG); + this.contextStack.push(array); + this.nestingInfoStack.push("Bag"); + } else if ("Alt".equals(localName)) { + XMPArray array = new XMPArray(XMPArrayType.ALT); + this.contextStack.push(array); + this.nestingInfoStack.push("Alt"); + } else if ("li".equals(localName)) { + //nop, handle in endElement() + } else if ("value".equals(localName)) { + QName name = new QName(uri, qName); + this.contextStack.push(name); + this.nestingInfoStack.push("prop:" + name); + } else { + throw new SAXException("Unexpected element in the RDF namespace: " + localName); + } + } else { + if (getCurrentPropName() != null) { + //Structure (shorthand form) + startStructure(); + } + QName name = new QName(uri, qName); + this.contextStack.push(name); + this.nestingInfoStack.push("prop:" + name); + } + } + + private void startStructure() { + //a structured property is the parent + XMPStructure struct = new XMPStructure(); + this.contextStack.push(struct); + this.nestingInfoStack.push("struct"); + } + + /** {@inheritDoc} */ + public void endElement(String uri, String localName, String qName) throws SAXException { + Attributes atts = (Attributes)attributesStack.pop(); + if (XMPConstants.XMP_NAMESPACE.equals(uri)) { + //nop + } else if (XMPConstants.RDF_NAMESPACE.equals(uri) && !"value".equals(localName)) { + if ("li".equals(localName)) { + XMPStructure struct = getCurrentStructure(); + if (struct != null) { + //Pop the structure + this.contextStack.pop(); + this.nestingInfoStack.pop(); + getCurrentArray(true).add(struct); + } else { + String s = content.toString().trim(); + if (s.length() > 0) { + String lang = atts.getValue(XMPConstants.XML_NS, "lang"); + if (lang != null) { + getCurrentArray(true).add(s, lang); + } else { + getCurrentArray(true).add(s); + } + } else { + String res = atts.getValue(XMPConstants.RDF_NAMESPACE, + "resource"); + if (res != null) { + try { + URI resource = new URI(res); + getCurrentArray(true).add(resource); + } catch (URISyntaxException e) { + throw new SAXException("rdf:resource value is not a well-formed URI", e); + } + } + } + } + } else if ("Description".equals(localName)) { + /* + if (isInStructure()) { + //Description is indicating a structure + //this.currentProperties = (PropertyAccess)propertiesStack.pop(); + this.nestingInfoStack.pop(); + }*/ + } else { + //nop, don't pop stack so the parent element has access + } + } else { + XMPProperty prop; + QName name; + if (hasComplexContent()) { + //Pop content of property + Object obj = this.contextStack.pop(); + this.nestingInfoStack.pop(); + + name = popCurrentPropName(); + + if (obj instanceof XMPComplexValue) { + XMPComplexValue complexValue = (XMPComplexValue)obj; + prop = new XMPProperty(name, complexValue); + } else { + throw new UnsupportedOperationException("NYI"); + } + } else { + name = popCurrentPropName(); + + String s = content.toString().trim(); + prop = new XMPProperty(name, s); + String lang = atts.getValue(XMPConstants.XML_NS, "lang"); + String res = atts.getValue(XMPConstants.RDF_NAMESPACE, "resource"); + if (lang != null) { + prop.setXMLLang(lang); + } + if (res != null) { + try { + URI resource = new URI(res); + prop.setValue(resource); + } catch (URISyntaxException e) { + throw new SAXException("rdf:resource value is not a well-formed URI", e); + } + } + } + if (prop.getName() == null) { + throw new IllegalStateException("No content in XMP property"); + } + assert getCurrentProperties() != null : "no current property"; + getCurrentProperties().setProperty(prop); + } + + content.setLength(0); //Reset text buffer (see characters()) + super.endElement(uri, localName, qName); + } + + /* + private boolean isInStructure() { + return !propertiesStack.isEmpty(); + } + */ + + /** {@inheritDoc} */ + public void characters(char[] ch, int start, int length) throws SAXException { + content.append(ch, start, length); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/xmp/XMPPacketParser.java b/src/main/java/org/apache/xmlgraphics/xmp/XMPPacketParser.java new file mode 100644 index 0000000..22f03fc --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/xmp/XMPPacketParser.java @@ -0,0 +1,128 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: XMPPacketParser.java 1681108 2015-05-22 13:26:12Z ssteiner $ */ + +package org.apache.xmlgraphics.xmp; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; + +import javax.xml.transform.TransformerException; +import javax.xml.transform.stream.StreamSource; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.io.output.ByteArrayOutputStream; + +/** + * This class is a parser for XMP packets. By default, it tries to locate the first XMP packet + * it finds and parses it. + *

    + * Important: Before you use this class to look for an XMP packet in some random file, please read + * the chapter on "Scanning Files for XMP Packets" in the XMP specification! + */ +public final class XMPPacketParser { + + private XMPPacketParser() { + } + + private static final byte[] PACKET_HEADER; + private static final byte[] PACKET_HEADER_END; + private static final byte[] PACKET_TRAILER; + + static { + try { + PACKET_HEADER = "".getBytes("US-ASCII"); + PACKET_TRAILER = "= 0) { + if (b == match[found]) { + found++; + if (found == len) { + return true; + } + } else { + if (out != null) { + if (found > 0) { + out.write(match, 0, found); + } + out.write(b); + } + found = 0; + } + } + return false; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/xmp/XMPParser.java b/src/main/java/org/apache/xmlgraphics/xmp/XMPParser.java new file mode 100644 index 0000000..7e661de --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/xmp/XMPParser.java @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: XMPParser.java 1878394 2020-06-02 13:18:41Z ssteiner $ */ + +package org.apache.xmlgraphics.xmp; + +import java.net.URL; + +import javax.xml.XMLConstants; +import javax.xml.transform.Source; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.sax.SAXResult; +import javax.xml.transform.stream.StreamSource; + +/** + * The XMP parser. + */ +public final class XMPParser { + + private XMPParser() { + } + + /** + * Parses an XMP file. + * @param url the URL to load the file from + * @return the parsed Metadata object + * @throws TransformerException if an error occurs while parsing the file + */ + public static Metadata parseXMP(URL url) throws TransformerException { + return parseXMP(new StreamSource(url.toExternalForm())); + } + + /** + * Parses an XMP file. + * @param src a JAXP Source object where the XMP file can be loaded from + * @return the parsed Metadata object + * @throws TransformerException if an error occurs while parsing the file + */ + public static Metadata parseXMP(Source src) throws TransformerException { + TransformerFactory tFactory = TransformerFactory.newInstance(); + tFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + tFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, ""); + Transformer transformer = tFactory.newTransformer(); + XMPHandler handler = createXMPHandler(); + SAXResult res = new SAXResult(handler); + transformer.transform(src, res); + return handler.getMetadata(); + } + + /** + * Creates and returns an XMPHandler for passive XMP parsing. + * @return the requested XMPHandler + */ + public static XMPHandler createXMPHandler() { + return new XMPHandler(); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/xmp/XMPProperty.java b/src/main/java/org/apache/xmlgraphics/xmp/XMPProperty.java new file mode 100644 index 0000000..229ac3a --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/xmp/XMPProperty.java @@ -0,0 +1,222 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: XMPProperty.java 1731596 2016-02-22 08:28:54Z gadams $ */ + +package org.apache.xmlgraphics.xmp; + +import java.net.URI; + +import java.util.Iterator; +import java.util.Map; + +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.AttributesImpl; + +import org.apache.xmlgraphics.util.QName; +import org.apache.xmlgraphics.util.XMLizable; + +/** + * This class is the base class for all XMP properties. + */ +public class XMPProperty implements XMLizable { + + private QName name; + private Object value; + private String xmllang; + private Map qualifiers; + + /** + * Creates a new XMP property. + * @param name the name of the property + * @param value the value for the property + */ + public XMPProperty(QName name, Object value) { + this.name = name; + this.value = value; + } + + /** @return the qualified name of the property (namespace URI + local name) */ + public QName getName() { + return this.name; + } + + /** @return the namespace URI of the property */ + public String getNamespace() { + return getName().getNamespaceURI(); + } + + /** + * Sets the value of the property + * @param value the new value + */ + public void setValue(Object value) { + this.value = value; + } + + /** + * @return the property value (can be a normal Java object (normally a String) or a descendant + * of XMPComplexValue. + */ + public Object getValue() { + return this.value; + } + + /** + * Sets the xml:lang value for this property + * @param lang the language ("x-default" for the default language, null to make the value + * language-independent) + */ + public void setXMLLang(String lang) { + this.xmllang = lang; + } + + /** + * @return the language for language-dependent values ("x-default" for the default language) + */ + public String getXMLLang() { + return this.xmllang; + } + + /** + * Indicates whether the property is an array. + * @return true if the property is an array + */ + public boolean isArray() { + return value instanceof XMPArray; + } + + /** @return the XMPArray for an array or null if the value is not an array. */ + public XMPArray getArrayValue() { + return (isArray() ? (XMPArray)value : null); + } + + /** + * Converts a simple value to an array of a given type if the value is not already an array. + * @param type the desired type of array + * @return the array value + */ + public XMPArray convertSimpleValueToArray(XMPArrayType type) { + if (getArrayValue() == null) { + XMPArray array = new XMPArray(type); + if (getXMLLang() != null) { + array.add(getValue().toString(), getXMLLang()); + } else { + array.add(getValue()); + } + setValue(array); + setXMLLang(null); + return array; + } else { + return getArrayValue(); + } + } + + /** @return the XMPStructure for a structure or null if the value is not a structure. */ + public PropertyAccess getStructureValue() { + return (value instanceof XMPStructure ? (XMPStructure)value : null); + } + + private boolean hasPropertyQualifiers() { + return (this.qualifiers == null) || (this.qualifiers.size() == 0); + } + + /** + * Indicates whether this property is actually not a structure, but a normal property with + * property qualifiers. If this method returns true, this structure can be converted to + * an simple XMPProperty using the simplify() method. + * @return true if this property is a structure property with property qualifiers + */ + public boolean isQualifiedProperty() { + PropertyAccess props = getStructureValue(); + if (props != null) { + XMPProperty rdfValue = props.getValueProperty(); + return (rdfValue != null); + } else { + return hasPropertyQualifiers(); + } + } + + public void simplify() { + PropertyAccess props = getStructureValue(); + if (props != null) { + XMPProperty rdfValue = props.getValueProperty(); + if (rdfValue != null) { + if (hasPropertyQualifiers()) { + throw new IllegalStateException("Illegal internal state" + + " (qualifiers present on non-simplified property)"); + } + XMPProperty prop = new XMPProperty(getName(), rdfValue); + Iterator iter = props.iterator(); + while (iter.hasNext()) { + QName name = (QName)iter.next(); + if (!XMPConstants.RDF_VALUE.equals(name)) { + prop.setPropertyQualifier(name, props.getProperty(name)); + } + } + props.setProperty(prop); + } + } + } + + + private void setPropertyQualifier(QName name, XMPProperty property) { + if (this.qualifiers == null) { + this.qualifiers = new java.util.HashMap(); + } + this.qualifiers.put(name, property); + } + + private String getEffectiveQName() { + String prefix = getName().getPrefix(); + if (prefix == null || "".equals(prefix)) { + XMPSchema schema = XMPSchemaRegistry.getInstance().getSchema(getNamespace()); + prefix = schema.getPreferredPrefix(); + } + return prefix + ":" + getName().getLocalName(); + } + + /** @see org.apache.xmlgraphics.util.XMLizable#toSAX(org.xml.sax.ContentHandler) */ + public void toSAX(ContentHandler handler) throws SAXException { + AttributesImpl atts = new AttributesImpl(); + String qName = getEffectiveQName(); + if (value instanceof URI) { + atts.addAttribute(XMPConstants.RDF_NAMESPACE, "resource", "rdf:resource", "CDATA", ((URI)value).toString()); + } + handler.startElement(getName().getNamespaceURI(), + getName().getLocalName(), qName, atts); + if (value instanceof XMPComplexValue) { + XMPComplexValue cv = ((XMPComplexValue)value); + cv.toSAX(handler); + } else if (!(value instanceof URI)) { + char[] chars = value.toString().toCharArray(); + handler.characters(chars, 0, chars.length); + } + handler.endElement(getName().getNamespaceURI(), + getName().getLocalName(), qName); + } + + /** @see java.lang.Object#toString() */ + public String toString() { + StringBuffer sb = new StringBuffer("XMP Property "); + sb.append(getName()).append(": "); + sb.append(getValue()); + return sb.toString(); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/xmp/XMPSchema.java b/src/main/java/org/apache/xmlgraphics/xmp/XMPSchema.java new file mode 100644 index 0000000..1c70d7e --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/xmp/XMPSchema.java @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: XMPSchema.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.xmp; + +import org.apache.xmlgraphics.util.QName; +import org.apache.xmlgraphics.xmp.merge.MergeRuleSet; + +/** + * Base class for schema implementations that provide user-friendly access to XMP values. + */ +public class XMPSchema { + + private static MergeRuleSet defaultMergeRuleSet = new MergeRuleSet(); + + private String namespace; + private String prefix; + + /** + * Constructs a new XMP schema object. + * @param namespace the namespace URI for the schema + * @param preferredPrefix the preferred prefix for the schema + */ + public XMPSchema(String namespace, String preferredPrefix) { + this.namespace = namespace; + this.prefix = preferredPrefix; + } + + /** @return the namespace URI of the schema */ + public String getNamespace() { + return this.namespace; + } + + /** @return the preferred prefix of the schema */ + public String getPreferredPrefix() { + return this.prefix; + } + + /** + * Returns the QName for a property of this schema. + * @param propName the property name + * @return the QName for the property + */ + protected QName getQName(String propName) { + return new QName(getNamespace(), propName); + } + + /** @return the default merge rule set for this XMP schema. */ + public MergeRuleSet getDefaultMergeRuleSet() { + return defaultMergeRuleSet; + } +} diff --git a/src/main/java/org/apache/xmlgraphics/xmp/XMPSchemaAdapter.java b/src/main/java/org/apache/xmlgraphics/xmp/XMPSchemaAdapter.java new file mode 100644 index 0000000..9a41eba --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/xmp/XMPSchemaAdapter.java @@ -0,0 +1,449 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: XMPSchemaAdapter.java 1829045 2018-04-13 09:22:33Z ssteiner $ */ + +package org.apache.xmlgraphics.xmp; + +import java.util.Date; +import java.util.TimeZone; + +import org.apache.xmlgraphics.util.DateFormatUtil; +import org.apache.xmlgraphics.util.QName; + +/** + * Base class for schema-specific adapters that provide user-friendly access to XMP values. + */ +public class XMPSchemaAdapter { + + /** the Metadata object this schema instance operates on */ + protected Metadata meta; + private XMPSchema schema; + private boolean compact = true; + + /** + * Main constructor. + * @param meta the Metadata object to wrao + * @param schema the XMP schema for which this adapter was written + */ + public XMPSchemaAdapter(Metadata meta, XMPSchema schema) { + if (meta == null) { + throw new NullPointerException("Parameter meta must not be null"); + } + if (schema == null) { + throw new NullPointerException("Parameter schema must not be null"); + } + this.meta = meta; + this.schema = schema; + } + + /** @return the XMP schema associated with this adapter */ + public XMPSchema getSchema() { + return this.schema; + } + + /** + * Returns the QName for a given property + * @param propName the property name + * @return the resulting QName + */ + protected QName getQName(String propName) { + return new QName(getSchema().getNamespace(), getSchema().getPreferredPrefix(), propName); + } + + /** + * Adds a String value to an array. + * @param propName the property name + * @param value the String value + * @param arrayType the type of array to operate on + */ + private void addStringToArray(String propName, String value, XMPArrayType arrayType) { + if (value == null || value.length() == 0) { + throw new IllegalArgumentException("'" + propName + "' value must not be empty"); + } + addObjectToArray(propName, value, arrayType); + } + + /** + * Adds a Object value to an array. + * @param propName the property name + * @param value the Object value + * @param arrayType the type of array to operate on + */ + protected void addObjectToArray(String propName, Object value, XMPArrayType arrayType) { + if (value == null) { + throw new IllegalArgumentException("'" + propName + "' value must not be null"); + } + QName name = getQName(propName); + XMPProperty prop = meta.getProperty(name); + if (prop == null) { + prop = new XMPProperty(name, value); + meta.setProperty(prop); + if (!compact) { + prop.convertSimpleValueToArray(arrayType); + } + } else { + prop.convertSimpleValueToArray(arrayType); + prop.getArrayValue().add(value); + } + } + + /** + * Removes a value from an array. + * @param propName the name of the property + * @param value the value to be removed + * @return true if the value was removed, false if it was not found + */ + protected boolean removeStringFromArray(String propName, String value) { + if (value == null) { + return false; + } + QName name = getQName(propName); + XMPProperty prop = meta.getProperty(name); + if (prop != null) { + if (prop.isArray()) { + XMPArray arr = prop.getArrayValue(); + boolean removed = arr.remove(value); + if (arr.isEmpty()) { + meta.removeProperty(name); + } + return removed; + } else { + Object currentValue = prop.getValue(); + if (value.equals(currentValue)) { + meta.removeProperty(name); + return true; + } + } + } + return false; + } + + /** + * Adds a String value to an ordered array. + * @param propName the property name + * @param value the String value + */ + protected void addStringToSeq(String propName, String value) { + addStringToArray(propName, value, XMPArrayType.SEQ); + } + + /** + * Adds a String value to an unordered array. + * @param propName the property name + * @param value the String value + */ + protected void addStringToBag(String propName, String value) { + addStringToArray(propName, value, XMPArrayType.BAG); + } + + /** + * Formats a Date using ISO 8601 format in the default time zone. + * @param dt the date + * @return the formatted date + */ + public static String formatISO8601Date(Date dt) { + return formatISO8601Date(dt, TimeZone.getDefault()); + } + + /** + * Formats a Date using ISO 8601 format in the given time zone. + * @param dt the date + * @param tz the time zone + * @return the formatted date + */ + public static String formatISO8601Date(Date dt, TimeZone tz) { + return DateFormatUtil.formatISO8601(dt, tz); + } + + /** + * Adds a date value to an ordered array. + * @param propName the property name + * @param value the date value + */ + protected void addDateToSeq(String propName, Date value) { + String dt = formatISO8601Date(value); + addStringToSeq(propName, dt); + } + + /** + * Set a date value. + * @param propName the property name + * @param value the date value + */ + protected void setDateValue(String propName, Date value) { + String dt = formatISO8601Date(value); + setValue(propName, dt); + } + + /** + * Returns a date value. + * @param propName the property name + * @return the date value or null if the value is not set + */ + protected Date getDateValue(String propName) { + String dt = getValue(propName); + if (dt == null) { + return null; + } else { + return DateFormatUtil.parseISO8601Date(dt); + } + } + + /** + * Sets a language-dependent value. + * @param propName the property name + * @param lang the language ("x-default" or null for the default language) + * @param value the value + */ + protected void setLangAlt(String propName, String lang, String value) { + if (lang == null) { + lang = XMPConstants.DEFAULT_LANGUAGE; + } + QName name = getQName(propName); + XMPProperty prop = meta.getProperty(name); + XMPArray array; + if (prop == null) { + if (value != null && value.length() > 0) { + prop = new XMPProperty(name, value); + prop.setXMLLang(lang); + meta.setProperty(prop); + } + } else { + prop.convertSimpleValueToArray(XMPArrayType.ALT); + array = prop.getArrayValue(); + array.removeLangValue(lang); + if (value != null && value.length() > 0) { + array.add(value, lang); + } else { + if (array.isEmpty()) { + meta.removeProperty(name); + } + } + } + } + + /** + * Sets a simple value. + * @param propName the property name + * @param value the value + */ + protected void setValue(String propName, String value) { + QName name = getQName(propName); + XMPProperty prop = meta.getProperty(name); + if (value != null && value.length() > 0) { + if (prop != null) { + prop.setValue(value); + } else { + prop = new XMPProperty(name, value); + meta.setProperty(prop); + } + } else { + if (prop != null) { + meta.removeProperty(name); + } + } + } + + /** + * Returns a simple value. + * @param propName the property name + * @return the requested value or null if it isn't set + */ + protected String getValue(String propName) { + QName name = getQName(propName); + XMPProperty prop = meta.getProperty(name); + if (prop == null) { + return null; + } else { + return prop.getValue().toString(); + } + } + + /** + * Removes a language-dependent value from an alternative array. + * @param lang the language ("x-default" for the default language) + * @param propName the property name + * @return the removed value + */ + protected String removeLangAlt(String lang, String propName) { + QName name = getQName(propName); + XMPProperty prop = meta.getProperty(name); + XMPArray array; + if (prop != null && lang != null) { + array = prop.getArrayValue(); + if (array != null) { + String removed = array.removeLangValue(lang); + if (array.isEmpty()) { + meta.removeProperty(name); + } + return removed; + } else { + String removed = prop.getValue().toString(); + if (lang.equals(prop.getXMLLang())) { + meta.removeProperty(name); + } + return removed; + } + } + return null; + } + + /** + * Returns a language-dependent value. If the value in the requested language is not available + * the value for the default language is returned. + * @param lang the language ("x-default" for the default language) + * @param propName the property name + * @return the requested value + */ + protected String getLangAlt(String lang, String propName) { + XMPProperty prop = meta.getProperty(getQName(propName)); + XMPArray array; + if (prop == null) { + return null; + } else { + array = prop.getArrayValue(); + if (array != null) { + return array.getLangValue(lang); + } else { + return prop.getValue().toString(); + } + } + } + + /** + * Finds a structure that matches a given qualifier. + * @param propName the property name + * @param qualifier the qualifier + * @param qualifierValue the qualifier value + * @return the structure if a match was found (or null if no match was found) + */ + protected PropertyAccess findQualifiedStructure(String propName, + QName qualifier, String qualifierValue) { + XMPProperty prop = meta.getProperty(getQName(propName)); + XMPArray array; + if (prop != null) { + array = prop.getArrayValue(); + if (array != null) { + for (int i = 0, c = array.getSize(); i < c; i++) { + Object value = array.getValue(i); + if (value instanceof PropertyAccess) { + PropertyAccess pa = (PropertyAccess)value; + XMPProperty q = pa.getProperty(qualifier); + if (q != null && q.getValue().equals(qualifierValue)) { + return pa; + } + } + } + } else if (prop.getStructureValue() != null) { + PropertyAccess pa = prop.getStructureValue(); + XMPProperty q = pa.getProperty(qualifier); + if (q != null && q.getValue().equals(qualifierValue)) { + return pa; + } + } + } + return null; + } + + /** + * Finds a value that matches a given qualifier. + * @param propName the property name + * @param qualifier the qualifier + * @param qualifierValue the qualifier value + * @return the value if a match was found (or null if no match was found) + */ + protected Object findQualifiedValue(String propName, + QName qualifier, String qualifierValue) { + PropertyAccess pa = findQualifiedStructure(propName, qualifier, qualifierValue); + if (pa != null) { + XMPProperty rdfValue = pa.getValueProperty(); + if (rdfValue != null) { + return rdfValue.getValue(); + } + } + return null; + } + + /** + * Returns an object array representation of the property's values. + * @param propName the property name + * @return the object array or null if the property isn't set + */ + protected Object[] getObjectArray(String propName) { + XMPProperty prop = meta.getProperty(getQName(propName)); + if (prop == null) { + return null; + } + XMPArray array = prop.getArrayValue(); + if (array != null) { + return array.toObjectArray(); + } else { + return new Object[] {prop.getValue()}; + } + } + + /** + * Returns a String array representation of the property's values. Complex values are converted + * to Strings using the toString() method. + * @param propName the property name + * @return the String array or null if the property isn't set + */ + protected String[] getStringArray(String propName) { + Object[] arr = getObjectArray(propName); + if (arr == null) { + return null; + } + String[] res = new String[arr.length]; + for (int i = 0, c = res.length; i < c; i++) { + Object o = arr[i]; + if (o instanceof PropertyAccess) { + XMPProperty prop = ((PropertyAccess)o).getValueProperty(); + res[i] = prop.getValue().toString(); + } else { + res[i] = o.toString(); + } + } + return res; + } + + /** + * Returns a Date array representation of the property's values. + * @param propName the property name + * @return the Date array or null if the property isn't set + */ + protected Date[] getDateArray(String propName) { + Object[] arr = getObjectArray(propName); + if (arr == null) { + return null; + } + Date[] res = new Date[arr.length]; + for (int i = 0, c = res.length; i < c; i++) { + Object obj = arr[i]; + if (obj instanceof Date) { + res[i] = (Date) ((Date) obj).clone(); + } else { + res[i] = DateFormatUtil.parseISO8601Date(obj.toString()); + } + } + return res; + } + + public void setCompact(boolean c) { + compact = c; + } +} diff --git a/src/main/java/org/apache/xmlgraphics/xmp/XMPSchemaRegistry.java b/src/main/java/org/apache/xmlgraphics/xmp/XMPSchemaRegistry.java new file mode 100644 index 0000000..f6a43e7 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/xmp/XMPSchemaRegistry.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: XMPSchemaRegistry.java 1685768 2015-06-16 11:46:06Z ssteiner $ */ + +package org.apache.xmlgraphics.xmp; + +import java.util.Map; + +import org.apache.xmlgraphics.xmp.schemas.DublinCoreSchema; +import org.apache.xmlgraphics.xmp.schemas.XMPBasicSchema; +import org.apache.xmlgraphics.xmp.schemas.pdf.AdobePDFSchema; +import org.apache.xmlgraphics.xmp.schemas.pdf.PDFAXMPSchema; +import org.apache.xmlgraphics.xmp.schemas.pdf.PDFUAXMPSchema; +import org.apache.xmlgraphics.xmp.schemas.pdf.PDFVTXMPSchema; +import org.apache.xmlgraphics.xmp.schemas.pdf.PDFXXMPSchema; +import org.apache.xmlgraphics.xmp.schemas.pdf.XAPMMXMPSchema; + +/** + * This class is a registry of XMP schemas. It's implemented as a singleton. + */ +public final class XMPSchemaRegistry { + + private static XMPSchemaRegistry instance = new XMPSchemaRegistry(); + + private Map schemas = new java.util.HashMap(); + + private XMPSchemaRegistry() { + init(); + } + + /** @return the singleton instance of the XMP schema registry. */ + public static XMPSchemaRegistry getInstance() { + return instance; + } + + private void init() { + addSchema(new DublinCoreSchema()); + addSchema(new PDFAXMPSchema()); + addSchema(new XMPBasicSchema()); + addSchema(new AdobePDFSchema()); + addSchema(new PDFXXMPSchema()); + addSchema(new PDFVTXMPSchema()); + addSchema(new XAPMMXMPSchema()); + addSchema(new PDFUAXMPSchema()); + } + + /** + * Adds an XMP schema to the registry. + * @param schema the XMP schema + */ + public void addSchema(XMPSchema schema) { + schemas.put(schema.getNamespace(), schema); + } + + /** + * Returns the XMP schema object for a given namespace. + * @param namespace the namespace URI + * @return the XMP schema or null if none is available + */ + public XMPSchema getSchema(String namespace) { + return (XMPSchema)schemas.get(namespace); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/xmp/XMPSerializer.java b/src/main/java/org/apache/xmlgraphics/xmp/XMPSerializer.java new file mode 100644 index 0000000..4fbe1f9 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/xmp/XMPSerializer.java @@ -0,0 +1,117 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: XMPSerializer.java 1345683 2012-06-03 14:50:33Z gadams $ */ + +package org.apache.xmlgraphics.xmp; + +import java.io.OutputStream; + +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Result; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerConfigurationException; +import javax.xml.transform.sax.SAXTransformerFactory; +import javax.xml.transform.sax.TransformerHandler; +import javax.xml.transform.stream.StreamResult; + +import org.xml.sax.SAXException; + +/** + * Serializes an XMP tree to XML or to an XMP packet. + */ +public final class XMPSerializer { + + private XMPSerializer() { + } + + private static final String DEFAULT_ENCODING = "UTF-8"; + + /** + * Writes the in-memory representation of the XMP metadata to a JAXP Result. + * @param meta the metadata + * @param res the JAXP Result to write to + * @throws TransformerConfigurationException if an error occurs setting up the XML + * infrastructure. + * @throws SAXException if a SAX-related problem occurs while writing the XML + */ + public static void writeXML(Metadata meta, Result res) + throws TransformerConfigurationException, SAXException { + writeXML(meta, res, false, false); + } + + /** + * Writes the in-memory representation of the XMP metadata to an OutputStream as an XMP packet. + * @param meta the metadata + * @param out the stream to write to + * @param readOnlyXMP true if the generated XMP packet should be read-only + * @throws TransformerConfigurationException if an error occurs setting up the XML + * infrastructure. + * @throws SAXException if a SAX-related problem occurs while writing the XML + */ + public static void writeXMPPacket(Metadata meta, OutputStream out, boolean readOnlyXMP) + throws TransformerConfigurationException, SAXException { + StreamResult res = new StreamResult(out); + writeXML(meta, res, true, readOnlyXMP); + + } + + private static void writeXML(Metadata meta, Result res, + boolean asXMPPacket, boolean readOnlyXMP) + throws TransformerConfigurationException, SAXException { + SAXTransformerFactory tFactory = (SAXTransformerFactory)SAXTransformerFactory.newInstance(); + TransformerHandler handler = tFactory.newTransformerHandler(); + Transformer transformer = handler.getTransformer(); + if (asXMPPacket) { + transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); + } + transformer.setOutputProperty(OutputKeys.ENCODING, DEFAULT_ENCODING); + try { + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + } catch (IllegalArgumentException iae) { + //INDENT key is not supported by implementation. That's not tragic, so just ignore. + } + handler.setResult(res); + handler.startDocument(); + if (asXMPPacket) { + handler.processingInstruction("xpacket", + "begin=\"\uFEFF\" id=\"W5M0MpCehiHzreSzNTczkc9d\""); + } + meta.toSAX(handler); + if (asXMPPacket) { + if (readOnlyXMP) { + handler.processingInstruction("xpacket", "end=\"r\""); + } else { + //Create padding string (40 * 101 characters is more or less the recommended 4KB) + StringBuffer sb = new StringBuffer(101); + sb.append('\n'); + for (int i = 0; i < 100; i++) { + sb.append(" "); + } + char[] padding = sb.toString().toCharArray(); + for (int i = 0; i < 40; i++) { + handler.characters(padding, 0, padding.length); + } + handler.characters(new char[] {'\n'}, 0, 1); + handler.processingInstruction("xpacket", "end=\"w\""); + } + + } + handler.endDocument(); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/xmp/XMPStructure.java b/src/main/java/org/apache/xmlgraphics/xmp/XMPStructure.java new file mode 100644 index 0000000..2596b1e --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/xmp/XMPStructure.java @@ -0,0 +1,106 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: XMPStructure.java 1804124 2017-08-04 14:13:54Z ssteiner $ */ + +package org.apache.xmlgraphics.xmp; + +import java.util.Iterator; +import java.util.Map; + +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.AttributesImpl; + +import org.apache.xmlgraphics.util.QName; + +/** + * Represents an XMP structure as defined by the XMP specification. + */ +public class XMPStructure extends XMPComplexValue implements PropertyAccess { + + private Map properties = new java.util.HashMap(); + + /** + * Main constructor + */ + public XMPStructure() { + } + + /** {@inheritDoc} */ + public Object getSimpleValue() { + return null; + } + + /** {@inheritDoc} */ + public void setProperty(XMPProperty prop) { + properties.put(prop.getName(), prop); + } + + /** {@inheritDoc} */ + public XMPProperty getProperty(String uri, String localName) { + return getProperty(new QName(uri, localName)); + } + + /** {@inheritDoc} */ + public XMPProperty getValueProperty() { + return getProperty(XMPConstants.RDF_VALUE); + } + + /** {@inheritDoc} */ + public XMPProperty getProperty(QName name) { + XMPProperty prop = (XMPProperty)properties.get(name); + return prop; + } + + /** {@inheritDoc} */ + public XMPProperty removeProperty(QName name) { + return (XMPProperty)properties.remove(name); + } + + /** {@inheritDoc} */ + public int getPropertyCount() { + return this.properties.size(); + } + + /** {@inheritDoc} */ + public Iterator iterator() { + return this.properties.keySet().iterator(); + } + + /** {@inheritDoc} */ + public void toSAX(ContentHandler handler) throws SAXException { + AttributesImpl atts = new AttributesImpl(); + atts.clear(); + handler.startElement(XMPConstants.RDF_NAMESPACE, "RDF", "rdf:Description", atts); + + for (Object o : properties.values()) { + XMPProperty prop = (XMPProperty) o; + //if (prop.getName().getNamespaceURI().equals(ns)) { + prop.toSAX(handler); + //} + } + handler.endElement(XMPConstants.RDF_NAMESPACE, "RDF", "rdf:Description"); + } + + /** {@inheritDoc} */ + public String toString() { + return "XMP structure: " + getPropertyCount(); + } + + +} diff --git a/src/main/java/org/apache/xmlgraphics/xmp/merge/ArrayAddPropertyMerger.java b/src/main/java/org/apache/xmlgraphics/xmp/merge/ArrayAddPropertyMerger.java new file mode 100644 index 0000000..8c40125 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/xmp/merge/ArrayAddPropertyMerger.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ArrayAddPropertyMerger.java 892977 2009-12-21 21:08:42Z jeremias $ */ + +package org.apache.xmlgraphics.xmp.merge; + +import org.apache.xmlgraphics.xmp.Metadata; +import org.apache.xmlgraphics.xmp.XMPArray; +import org.apache.xmlgraphics.xmp.XMPArrayType; +import org.apache.xmlgraphics.xmp.XMPProperty; + +/** + * Merges properties by adding up all items from both ends into one SEQ array. + */ +public class ArrayAddPropertyMerger implements PropertyMerger { + + /** + * @see org.apache.xmlgraphics.xmp.merge.PropertyMerger#merge( + * org.apache.xmlgraphics.xmp.XMPProperty, org.apache.xmlgraphics.xmp.Metadata) + */ + public void merge(XMPProperty sourceProp, Metadata target) { + XMPProperty existing = target.getProperty(sourceProp.getName()); + if (existing == null) { + //simply copy over + target.setProperty(sourceProp); + } else { + XMPArray array = existing.convertSimpleValueToArray(XMPArrayType.SEQ); + XMPArray otherArray = sourceProp.getArrayValue(); + if (otherArray == null) { + if (sourceProp.getXMLLang() != null) { + array.add(sourceProp.getValue().toString(), sourceProp.getXMLLang()); + } else { + array.add(sourceProp.getValue()); + } + } else { + //TODO should be refined (xml:lang etc.) + for (int i = 0, c = otherArray.getSize(); i < c; i++) { + array.add(otherArray.getValue(i)); + } + } + } + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/xmp/merge/MergeRuleSet.java b/src/main/java/org/apache/xmlgraphics/xmp/merge/MergeRuleSet.java new file mode 100644 index 0000000..7c3aae0 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/xmp/merge/MergeRuleSet.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: MergeRuleSet.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.xmp.merge; + +import java.util.Map; + +import org.apache.xmlgraphics.util.QName; +import org.apache.xmlgraphics.xmp.XMPProperty; + +/** + * Represents a set of rules used to merge to XMP properties. By default, all properties are + * merged by replacing any existing values with the value from the source XMP. + */ +public class MergeRuleSet { + + private Map rules = new java.util.HashMap(); + private PropertyMerger defaultMerger = new ReplacePropertyMerger(); + + /** Main constructor. */ + public MergeRuleSet() { + } + + /** + * Returns the PropertyMerger that shall be used when merging the given property. + * @param prop the property to be merged + * @return the PropertyMerger to be used for merging the property + */ + public PropertyMerger getPropertyMergerFor(XMPProperty prop) { + PropertyMerger merger = (PropertyMerger)rules.get(prop.getName()); + return (merger != null ? merger : defaultMerger); + } + + /** + * Adds a merge rule to this set. + * @param propName the name of the property + * @param merger the property merger to be used for this property + */ + public void addRule(QName propName, PropertyMerger merger) { + rules.put(propName, merger); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/xmp/merge/NoReplacePropertyMerger.java b/src/main/java/org/apache/xmlgraphics/xmp/merge/NoReplacePropertyMerger.java new file mode 100644 index 0000000..754e230 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/xmp/merge/NoReplacePropertyMerger.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: NoReplacePropertyMerger.java 426584 2006-07-28 16:01:47Z jeremias $ */ + +package org.apache.xmlgraphics.xmp.merge; + +import org.apache.xmlgraphics.xmp.Metadata; +import org.apache.xmlgraphics.xmp.XMPProperty; + +/** + * A basic PropertyMerger which only sets a value in the target metadata if there's not already + * another value. + */ +public class NoReplacePropertyMerger implements PropertyMerger { + + /** + * @see org.apache.xmlgraphics.xmp.merge.PropertyMerger#merge( + * org.apache.xmlgraphics.xmp.XMPProperty, org.apache.xmlgraphics.xmp.Metadata) + */ + public void merge(XMPProperty sourceProp, Metadata target) { + XMPProperty prop = target.getProperty(sourceProp.getName()); + if (prop == null) { + target.setProperty(sourceProp); + } + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/xmp/merge/PropertyMerger.java b/src/main/java/org/apache/xmlgraphics/xmp/merge/PropertyMerger.java new file mode 100644 index 0000000..e813a39 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/xmp/merge/PropertyMerger.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PropertyMerger.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.xmp.merge; + +import org.apache.xmlgraphics.xmp.Metadata; +import org.apache.xmlgraphics.xmp.XMPProperty; + +/** + * Defines an interface the classes can implement to provide special property merging behaviour. + */ +public interface PropertyMerger { + + /** + * Merges a property into a given metadata object + * @param sourceProp the source property + * @param target the target metadata object + */ + void merge(XMPProperty sourceProp, Metadata target); + +} diff --git a/src/main/java/org/apache/xmlgraphics/xmp/merge/ReplacePropertyMerger.java b/src/main/java/org/apache/xmlgraphics/xmp/merge/ReplacePropertyMerger.java new file mode 100644 index 0000000..f4fa2c5 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/xmp/merge/ReplacePropertyMerger.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ReplacePropertyMerger.java 426584 2006-07-28 16:01:47Z jeremias $ */ + +package org.apache.xmlgraphics.xmp.merge; + +import org.apache.xmlgraphics.xmp.Metadata; +import org.apache.xmlgraphics.xmp.XMPProperty; + +/** + * The most basic PropertyMerger which simply overwrites any existing value in the target metadata. + */ +public class ReplacePropertyMerger implements PropertyMerger { + + /** + * @see org.apache.xmlgraphics.xmp.merge.PropertyMerger#merge( + * org.apache.xmlgraphics.xmp.XMPProperty, org.apache.xmlgraphics.xmp.Metadata) + */ + public void merge(XMPProperty sourceProp, Metadata target) { + target.setProperty(sourceProp); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/xmp/merge/package.html b/src/main/java/org/apache/xmlgraphics/xmp/merge/package.html new file mode 100644 index 0000000..c954762 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/xmp/merge/package.html @@ -0,0 +1,23 @@ + + + +org.apache.xmlgraphics.commons.xmp.merge Package + +

    Classes for merging two XMP metadata documents.

    + + \ No newline at end of file diff --git a/src/main/java/org/apache/xmlgraphics/xmp/package.html b/src/main/java/org/apache/xmlgraphics/xmp/package.html new file mode 100644 index 0000000..cf41573 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/xmp/package.html @@ -0,0 +1,25 @@ + + + +org.apache.xmlgraphics.commons.xmp Package + +

    This package is an XMP metadata framework. For more information on +Adobe's XMP standard, visit: +Adobe's XMP website

    + + \ No newline at end of file diff --git a/src/main/java/org/apache/xmlgraphics/xmp/schemas/DublinCoreAdapter.java b/src/main/java/org/apache/xmlgraphics/xmp/schemas/DublinCoreAdapter.java new file mode 100644 index 0000000..7452160 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/xmp/schemas/DublinCoreAdapter.java @@ -0,0 +1,392 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DublinCoreAdapter.java 1804124 2017-08-04 14:13:54Z ssteiner $ */ + +package org.apache.xmlgraphics.xmp.schemas; + +import java.util.Date; + +import org.apache.xmlgraphics.xmp.Metadata; +import org.apache.xmlgraphics.xmp.XMPSchemaAdapter; +import org.apache.xmlgraphics.xmp.XMPSchemaRegistry; + +/** + * Schema adapter implementation for the Dublin Core schema. + *

    + * Note: In Adobe's XMP specification dc:subject is defined as "bag Text", but in PDF/A-1 it is + * defined as "Text". Here it is implemented as "bag Text". + */ +public class DublinCoreAdapter extends XMPSchemaAdapter { + + private static final String CONTRIBUTOR = "contributor"; + private static final String COVERAGE = "coverage"; + private static final String CREATOR = "creator"; + private static final String DATE = "date"; + private static final String DESCRIPTION = "description"; + private static final String FORMAT = "format"; + private static final String IDENTIFIER = "identifier"; + private static final String LANGUAGE = "language"; + private static final String PUBLISHER = "publisher"; + private static final String RELATION = "relation"; + private static final String RIGHTS = "rights"; + private static final String SOURCE = "source"; + private static final String SUBJECT = "subject"; + private static final String TITLE = "title"; + private static final String TYPE = "type"; + + /** + * Constructs a new adapter for Dublin Core around the given metadata object. + * @param meta the underlying metadata + */ + public DublinCoreAdapter(Metadata meta) { + super(meta, XMPSchemaRegistry.getInstance().getSchema(DublinCoreSchema.NAMESPACE)); + } + + /** + * Adds a new entry to the list of contributors (other than the authors). + * @param value the new value + */ + public void addContributor(String value) { + addStringToBag(CONTRIBUTOR, value); + } + + /** + * Removes an entry from the list of contributors. + * @param value the value to be removed + * @return the removed entry + */ + public boolean removeContributor(String value) { + return removeStringFromArray(CONTRIBUTOR, value); + } + + /** + * Returns an array of all contributors. + * @return a String array of all contributors (or null if not set) + */ + public String[] getContributors() { + return getStringArray(CONTRIBUTOR); + } + + /** + * Sets the extent or scope of the resource. + * @param value the new value. + */ + public void setCoverage(String value) { + setValue(COVERAGE, value); + } + + /** + * Returns the extent or scope of the resource. + * @return the property value (or null if not set) + */ + public String getCoverage() { + return getValue(COVERAGE); + } + + /** + * Adds a new entry to the list of creators (authors of the resource). + * @param value the new value + */ + public void addCreator(String value) { + addStringToSeq(CREATOR, value); + } + + /** + * Removes an entry from the list of creators (authors of the resource). + * @param value the value to be removed + * @return the removed entry + */ + public boolean removeCreator(String value) { + return removeStringFromArray(CREATOR, value); + } + + /** + * Returns an array of all creators. + * @return a String array of all creators (or null if not set) + */ + public String[] getCreators() { + return getStringArray(CREATOR); + } + + /** + * Adds a new entry to the list of dates indicating points in time something interesting + * happened to the resource. + * @param value the date value + */ + public void addDate(Date value) { + addDateToSeq(DATE, value); + } + + /** + * Returns a list of dates indicating point in time something interesting happened to the + * resource. + * @return the list of dates or null if no dates are set + */ + public Date[] getDates() { + return getDateArray(DATE); + } + + /** + * Returns a latest date indicating point in time something interesting happened to the + * resource. + * @return the last date or null + */ + public Date getDate() { + Date[] dates = getDates(); + if (dates != null) { + Date latest = null; + for (Date date : dates) { + if (latest == null || date.getTime() > latest.getTime()) { + latest = date; + } + } + return latest; + } else { + return null; + } + + } + + /** + * Sets the description of the content of the resource. + * @param lang the language of the value ("x-default" or null for the default language) + * @param value the new value + */ + public void setDescription(String lang, String value) { + setLangAlt(DESCRIPTION, lang, value); + } + + /** + * Returns the description of the content of the resource (in the default language). + * @return the description of the content of the resource (or null if not set) + */ + public String getDescription() { + return getDescription(null); + } + + /** + * Returns the description of the content of the resource in a language-dependant way. + * @param lang the language ("x-default" or null for the default language) + * @return the language-dependent value (or null if not set) + */ + public String getDescription(String lang) { + return getLangAlt(lang, DESCRIPTION); + } + + /** + * Sets the file format used when saving the resource. Tools and + * applications should set this property to the save format of the + * data. It may include appropriate qualifiers. + * @param value a MIME type + */ + public void setFormat(String value) { + setValue(FORMAT, value); + } + + /** + * Returns the file format used when saving this resource. + * @return the MIME type of the file format (or null if not set) + */ + public String getFormat() { + return getValue(FORMAT); + } + + /** + * Sets the unique identifier of the resource. + * @param value the new value + */ + public void setIdentifier(String value) { + setValue(IDENTIFIER, value); + } + + /** + * Returns the unique identifier of the resource. + * @return the unique identifier (or null if not set) + */ + public String getIdentifier() { + return getValue(IDENTIFIER); + } + + /** + * Adds a new entry to the list of languages (RFC 3066). + * @param value the new value + */ + public void addLanguage(String value) { + addStringToBag(LANGUAGE, value); + } + + /** + * Returns an array of languages. + * @return a String array of all languages (or null if not set) + */ + public String[] getLanguages() { + return getStringArray(LANGUAGE); + } + + /** + * Adds a new entry to the list of publishers. + * @param value the new value + */ + public void addPublisher(String value) { + addStringToBag(PUBLISHER, value); + } + + /** + * Returns an array of publishers. + * @return a String array of all publishers (or null if not set) + */ + public String[] getPublisher() { + return getStringArray(PUBLISHER); + } + + /** + * Adds a new entry to the list of relationships to other documents. + * @param value the new value + */ + public void addRelation(String value) { + addStringToBag(RELATION, value); + } + + /** + * Returns an array of all relationship to other documents. + * @return a String array of all relationships (or null if none are set) + */ + public String[] getRelations() { + return getStringArray(RELATION); + } + + /** + * Sets the informal rights statement. + * @param lang the language of the value ("x-default" or null for the default language) + * @param value the new value + */ + public void setRights(String lang, String value) { + setLangAlt(RIGHTS, lang, value); + } + + /** + * Returns the informal rights statement. + * @return the informal right statement (or null if not set) + */ + public String getRights() { + return getRights(null); + } + + /** + * Returns the informal rights statement in a language-dependant way. + * @param lang the language ("x-default" or null for the default language) + * @return the language-dependent value (or null if not set) + */ + public String getRights(String lang) { + return getLangAlt(lang, RIGHTS); + } + + /** + * Sets the unique identifier of the work from which this resource was derived. + * @param value the new value + */ + public void setSource(String value) { + setValue(SOURCE, value); + } + + /** + * Returns unique identifier of the work from which this resource was derived. + * @return the source (or null if not set) + */ + public String getSource() { + return getValue(SOURCE); + } + + /** + * Adds a new entry to the list of subjects (descriptive phrases or keywords that + * specify the topic of the content of the resource). + * @param value the new value + */ + public void addSubject(String value) { + addStringToBag(SUBJECT, value); + } + + /** + * Returns an array of all subjects. + * @return a String array of all subjects + */ + public String[] getSubjects() { + return getStringArray(SUBJECT); + } + + /** + * Sets the title of the resource (in the default language). + * @param value the new value + */ + public void setTitle(String value) { + setTitle(null, value); + } + + /** + * Sets the title of the resource. + * @param lang the language of the value ("x-default" or null for the default language) + * @param value the new value + */ + public void setTitle(String lang, String value) { + setLangAlt(TITLE, lang, value); + } + + /** + * Returns the title of the resource (in the default language). + * @return the title of the resource (in the default language) + */ + public String getTitle() { + return getTitle(null); + } + + /** + * Returns the title of the resource in a language-dependant way. + * @param lang the language ("x-default" or null for the default language) + * @return the language-dependent value (or null if not set) + */ + public String getTitle(String lang) { + return getLangAlt(lang, TITLE); + } + + /** + * Removes a title of the resource. + * @param lang the language variant to be removed + * @return the previously set value or null if this language variant wasn't set + */ + public String removeTitle(String lang) { + return removeLangAlt(lang, TITLE); + } + + /** + * Adds a new entry to the list of document types (for example: novel, poem or working paper). + * @param value the new value + */ + public void addType(String value) { + addStringToBag(TYPE, value); + } + + /** + * Returns an array of all document types. + * @return a String array of all document types (or null if not set) + */ + public String[] getTypes() { + return getStringArray(TYPE); + } + + +} diff --git a/src/main/java/org/apache/xmlgraphics/xmp/schemas/DublinCoreSchema.java b/src/main/java/org/apache/xmlgraphics/xmp/schemas/DublinCoreSchema.java new file mode 100644 index 0000000..eb0f8ae --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/xmp/schemas/DublinCoreSchema.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DublinCoreSchema.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.xmp.schemas; + +import org.apache.xmlgraphics.util.QName; +import org.apache.xmlgraphics.xmp.Metadata; +import org.apache.xmlgraphics.xmp.XMPConstants; +import org.apache.xmlgraphics.xmp.XMPSchema; +import org.apache.xmlgraphics.xmp.merge.ArrayAddPropertyMerger; +import org.apache.xmlgraphics.xmp.merge.MergeRuleSet; + +/** + * Schema class for Dublin Core. + */ +public class DublinCoreSchema extends XMPSchema { + + /** Namespace URI for Dublin Core */ + public static final String NAMESPACE = XMPConstants.DUBLIN_CORE_NAMESPACE; + + private static MergeRuleSet dcMergeRuleSet; + + static { + dcMergeRuleSet = new MergeRuleSet(); + //Dates are added up not replaced + dcMergeRuleSet.addRule(new QName(NAMESPACE, "date"), new ArrayAddPropertyMerger()); + } + + /** Creates a new schema instance for Dublin Core. */ + public DublinCoreSchema() { + super(NAMESPACE, "dc"); + } + + /** + * Creates and returns an adapter for this schema around the given metadata object. + * @param meta the metadata object + * @return the newly instantiated adapter + */ + public static DublinCoreAdapter getAdapter(Metadata meta) { + return new DublinCoreAdapter(meta); + } + + /** @see org.apache.xmlgraphics.xmp.XMPSchema#getDefaultMergeRuleSet() */ + public MergeRuleSet getDefaultMergeRuleSet() { + return dcMergeRuleSet; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/xmp/schemas/XMPBasicAdapter.java b/src/main/java/org/apache/xmlgraphics/xmp/schemas/XMPBasicAdapter.java new file mode 100644 index 0000000..c1a0b04 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/xmp/schemas/XMPBasicAdapter.java @@ -0,0 +1,173 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: XMPBasicAdapter.java 893190 2009-12-22 14:41:02Z jeremias $ */ + +package org.apache.xmlgraphics.xmp.schemas; + +import java.util.Date; + +import org.apache.xmlgraphics.xmp.Metadata; +import org.apache.xmlgraphics.xmp.PropertyAccess; +import org.apache.xmlgraphics.xmp.XMPArrayType; +import org.apache.xmlgraphics.xmp.XMPConstants; +import org.apache.xmlgraphics.xmp.XMPProperty; +import org.apache.xmlgraphics.xmp.XMPSchemaAdapter; +import org.apache.xmlgraphics.xmp.XMPSchemaRegistry; +import org.apache.xmlgraphics.xmp.XMPStructure; + +/** + * Schema adapter implementation for the XMP Basic schema. + */ +public class XMPBasicAdapter extends XMPSchemaAdapter { + + private static final String ADVISORY = "Advisory"; + private static final String BASE_URL = "BaseURL"; + private static final String CREATE_DATE = "CreateDate"; + private static final String CREATOR_TOOL = "CreatorTool"; + private static final String IDENTIFIER = "Identifier"; + private static final String LABEL = "Label"; + private static final String METADATA_DATE = "MetadataDate"; + private static final String MODIFY_DATE = "ModifyDate"; + private static final String NICKNAME = "Nickname"; + private static final String RATING = "Rating"; + private static final String THUMBNAILS = "Thumbnails"; + + /** + * Constructs a new adapter for XMP Basic around the given metadata object. + * @param meta the underlying metadata + */ + public XMPBasicAdapter(Metadata meta, String namespace) { + super(meta, XMPSchemaRegistry.getInstance().getSchema(namespace)); + } + + /** + * Sets the base URL for relative URLs in the document content. + * @param value the base URL + */ + public void setBaseUrl(String value) { + setValue(BASE_URL, value); + } + + /** + * Returns the base URL for relative URLs in the document content. + * @return the base URL + */ + public String getBaseUrl() { + return getValue(BASE_URL); + } + + /** + * Sets the date and time the resource was originally created. + * @param creationDate the creation date + */ + public void setCreateDate(Date creationDate) { + setDateValue(CREATE_DATE, creationDate); + } + + /** @return the date and time the resource was originally created */ + public Date getCreateDate() { + return getDateValue(CREATE_DATE); + } + + /** + * Sets the first known tool used to create the resource. + * @param value the creator tool + */ + public void setCreatorTool(String value) { + setValue(CREATOR_TOOL, value); + } + + /** @return the first known tool used to create the resource */ + public String getCreatorTool() { + return getValue(CREATOR_TOOL); + } + + /** + * Adds an identifier that unambiguously identify the resource within a given context. + * @param value the identifier value + */ + public void addIdentifier(String value) { + addStringToBag(IDENTIFIER, value); + } + + /** + * Sets a qualified identifier that unambiguously identify the resource within a given context. + * As qualifier, xmpidq:Scheme is used. + * @param value the identifier value + * @param qualifier the qualifier value (for xmpidq:Scheme) + */ + public void setIdentifier(String value, String qualifier) { + PropertyAccess pa = findQualifiedStructure(IDENTIFIER, + XMPBasicSchema.SCHEME_QUALIFIER, qualifier); + if (pa != null) { + pa.setProperty(new XMPProperty(XMPConstants.RDF_VALUE, value)); + } else { + XMPStructure struct = new XMPStructure(); + struct.setProperty(new XMPProperty(XMPConstants.RDF_VALUE, value)); + struct.setProperty(new XMPProperty(XMPBasicSchema.SCHEME_QUALIFIER, qualifier)); + addObjectToArray(IDENTIFIER, struct, XMPArrayType.BAG); + } + } + + /** + * Returns an array of all identifiers that unambiguously identify the resource within a + * given context. + * @return a String array of all identifiers (or null if not set) + */ + public String[] getIdentifiers() { + return getStringArray(IDENTIFIER); + } + + /** + * Returns an identifier that matches a given qualifier. + * As qualifier, xmpidq:Scheme is used. + * @param qualifier the qualifier + * @return the identifier (or null if no matching value was found) + */ + public String getIdentifier(String qualifier) { + Object value = findQualifiedValue(IDENTIFIER, XMPBasicSchema.SCHEME_QUALIFIER, qualifier); + return (value != null ? value.toString() : null); + } + + /** + * Sets the date and time the resource was last modified. + * @param modifyDate the modification date + */ + public void setModifyDate(Date modifyDate) { + setDateValue(MODIFY_DATE, modifyDate); + } + + /** @return the date and time the resource was last modified */ + public Date getModifyDate() { + return getDateValue(MODIFY_DATE); + } + + /** + * Sets the date and time any metadata for this resource was last changed. + * @param metadataDate the modification date for the metadata + */ + public void setMetadataDate(Date metadataDate) { + setDateValue(METADATA_DATE, metadataDate); + } + + /** @return the date and time the resource was originally created */ + public Date getMetadataDate() { + return getDateValue(METADATA_DATE); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/xmp/schemas/XMPBasicSchema.java b/src/main/java/org/apache/xmlgraphics/xmp/schemas/XMPBasicSchema.java new file mode 100644 index 0000000..b70c646 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/xmp/schemas/XMPBasicSchema.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: XMPBasicSchema.java 892977 2009-12-21 21:08:42Z jeremias $ */ + +package org.apache.xmlgraphics.xmp.schemas; + +import org.apache.xmlgraphics.util.QName; +import org.apache.xmlgraphics.xmp.Metadata; +import org.apache.xmlgraphics.xmp.XMPConstants; +import org.apache.xmlgraphics.xmp.XMPSchema; +import org.apache.xmlgraphics.xmp.merge.MergeRuleSet; +import org.apache.xmlgraphics.xmp.merge.NoReplacePropertyMerger; + +/** + * XMP schema for XMP Basic. + */ +public class XMPBasicSchema extends XMPSchema { + + /** Namespace URI for XMP Basic */ + public static final String NAMESPACE = XMPConstants.XMP_BASIC_NAMESPACE; + + /** Preferred prefix for XMP Basic */ + public static final String PREFERRED_PREFIX = "xmp"; + + /** Namespace URI for the qualifier for xmp:Identifier */ + public static final String QUALIFIER_NAMESPACE = "http://ns.adobe.com/xmp/Identifier/qual/1.0/"; + + /** The qualified name for the qualifier for xmp:Identifier */ + public static final QName SCHEME_QUALIFIER = new QName(QUALIFIER_NAMESPACE, "xmpidq:Scheme"); + + private static MergeRuleSet mergeRuleSet = new MergeRuleSet(); + + /** Creates a new schema instance for Dublin Core. */ + public XMPBasicSchema() { + super(NAMESPACE, PREFERRED_PREFIX); + } + + static { + //CreateDate shall be preserved if it exists + mergeRuleSet.addRule(new QName(NAMESPACE, "CreateDate"), new NoReplacePropertyMerger()); + } + + /** + * Creates and returns an adapter for this schema around the given metadata object. + * @param meta the metadata object + * @return the newly instantiated adapter + */ + public static XMPBasicAdapter getAdapter(Metadata meta) { + return new XMPBasicAdapter(meta, NAMESPACE); + } + + /** @see org.apache.xmlgraphics.xmp.XMPSchema#getDefaultMergeRuleSet() */ + public MergeRuleSet getDefaultMergeRuleSet() { + return mergeRuleSet; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/xmp/schemas/package.html b/src/main/java/org/apache/xmlgraphics/xmp/schemas/package.html new file mode 100644 index 0000000..c2654ed --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/xmp/schemas/package.html @@ -0,0 +1,26 @@ + + + +org.apache.xmlgraphics.commons.xmp.schemas Package + +

    Schema and schema adapter classes for accessing XMP metadata. The +schema classes provide information about a particular XMP schema. +The adapter classes are wrappers around a Metadata object and +provide easy Java read and write access to XMP metadata.

    + + \ No newline at end of file diff --git a/src/main/java/org/apache/xmlgraphics/xmp/schemas/pdf/AdobePDFAdapter.java b/src/main/java/org/apache/xmlgraphics/xmp/schemas/pdf/AdobePDFAdapter.java new file mode 100644 index 0000000..ce1972f --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/xmp/schemas/pdf/AdobePDFAdapter.java @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: AdobePDFAdapter.java 1666114 2015-03-12 10:00:27Z ssteiner $ */ + +package org.apache.xmlgraphics.xmp.schemas.pdf; + +import org.apache.xmlgraphics.xmp.Metadata; +import org.apache.xmlgraphics.xmp.XMPSchemaAdapter; +import org.apache.xmlgraphics.xmp.XMPSchemaRegistry; + +/** + * Schema adapter implementation for the Adobe PDF schema. + */ +public class AdobePDFAdapter extends XMPSchemaAdapter { + + private static final String KEYWORDS = "Keywords"; + private static final String PDFVERSION = "PDFVersion"; + private static final String PRODUCER = "Producer"; + private static final String TRAPPED = "Trapped"; + + /** + * Constructs a new adapter for Adobe PDF around the given metadata object. + * @param meta the underlying metadata + */ + public AdobePDFAdapter(Metadata meta, String namespace) { + super(meta, XMPSchemaRegistry.getInstance().getSchema(namespace)); + } + + /** @return the keywords */ + public String getKeywords() { + return getValue(KEYWORDS); + } + + /** + * Sets the keywords. + * @param value the keywords + */ + public void setKeywords(String value) { + setValue(KEYWORDS, value); + } + + /** @return the PDF version */ + public String getPDFVersion() { + return getValue(PDFVERSION); + } + + /** + * Sets the PDF version + * @param value the PDF version (ex. "1.4") + */ + public void setPDFVersion(String value) { + setValue(PDFVERSION, value); + } + + /** @return the name of the tool that produced the PDF document */ + public String getProducer() { + return getValue(PRODUCER); + } + + /** + * Sets the name of the tool that produced the PDF document + * @param value the producer + */ + public void setProducer(String value) { + setValue(PRODUCER, value); + } + + /** + * Sets if the pdf has trapping information True or False + * @param v the value + */ + public void setTrapped(String v) { + setValue(TRAPPED, v); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/xmp/schemas/pdf/AdobePDFSchema.java b/src/main/java/org/apache/xmlgraphics/xmp/schemas/pdf/AdobePDFSchema.java new file mode 100644 index 0000000..3871d4e --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/xmp/schemas/pdf/AdobePDFSchema.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: AdobePDFSchema.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.xmp.schemas.pdf; + +import org.apache.xmlgraphics.xmp.Metadata; +import org.apache.xmlgraphics.xmp.XMPConstants; +import org.apache.xmlgraphics.xmp.XMPSchema; +import org.apache.xmlgraphics.xmp.merge.MergeRuleSet; + +/** + * Adobe PDF XMP schema. + */ +public class AdobePDFSchema extends XMPSchema { + + /** Namespace URI for the Adobe PDF XMP schema */ + public static final String NAMESPACE = XMPConstants.ADOBE_PDF_NAMESPACE; + + private static MergeRuleSet mergeRuleSet = new MergeRuleSet(); + + /** Creates a new schema instance for Dublin Core. */ + public AdobePDFSchema() { + super(NAMESPACE, "pdf"); + } + + /** + * Creates and returns an adapter for this schema around the given metadata object. + * @param meta the metadata object + * @return the newly instantiated adapter + */ + public static AdobePDFAdapter getAdapter(Metadata meta) { + return new AdobePDFAdapter(meta, NAMESPACE); + } + + /** @see org.apache.xmlgraphics.xmp.XMPSchema#getDefaultMergeRuleSet() */ + public MergeRuleSet getDefaultMergeRuleSet() { + return mergeRuleSet; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/xmp/schemas/pdf/PDFAAdapter.java b/src/main/java/org/apache/xmlgraphics/xmp/schemas/pdf/PDFAAdapter.java new file mode 100644 index 0000000..ecc8bc8 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/xmp/schemas/pdf/PDFAAdapter.java @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PDFAAdapter.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.xmp.schemas.pdf; + +import org.apache.xmlgraphics.xmp.Metadata; +import org.apache.xmlgraphics.xmp.XMPSchemaAdapter; +import org.apache.xmlgraphics.xmp.XMPSchemaRegistry; + +/** + * Schema adapter implementation for both the old (deprecated) and the current PDF/A schema. + * The old namespace is still needed to make Adobe Acrobat happy. + */ +public class PDFAAdapter extends XMPSchemaAdapter { + + private static final String PART = "part"; + private static final String AMD = "amd"; + private static final String CONFORMANCE = "conformance"; + + /** + * Constructs a new adapter for PDF/A around the given metadata object. + * @param meta the underlying metadata + * @param namespace the namespace to access the schema (must be one of the PDF/A schema + * namespaces) + */ + public PDFAAdapter(Metadata meta, String namespace) { + super(meta, XMPSchemaRegistry.getInstance().getSchema(namespace)); + } + + /** + * Sets the PDF/A version identifier ("part"). + * @param value the version identifier ("1" for PDF/A-1) + */ + public void setPart(int value) { + setValue(PART, Integer.toString(value)); + } + + /** @return the PDF/A version identifier */ + public int getPart() { + return Integer.parseInt(getValue(PART)); + } + + /** + * Sets the PDF/A amendment identifier ("amd"). + * @param value the amendment identifiert + */ + public void setAmendment(String value) { + setValue(AMD, value); + } + + /** @return the PDF/A amendment identifier */ + public String getAmendment() { + return getValue(AMD); + } + + /** + * Sets the PDF/A conformance level. + * @param value the conformance level ("A" or "B" for PDF/A-1) + */ + public void setConformance(String value) { + setValue(CONFORMANCE, value); + } + + /** @return the PDF/A conformance level */ + public String getConformance() { + return getValue(CONFORMANCE); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/xmp/schemas/pdf/PDFAXMPSchema.java b/src/main/java/org/apache/xmlgraphics/xmp/schemas/pdf/PDFAXMPSchema.java new file mode 100644 index 0000000..2832022 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/xmp/schemas/pdf/PDFAXMPSchema.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PDFAXMPSchema.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.xmp.schemas.pdf; + +import org.apache.xmlgraphics.xmp.Metadata; +import org.apache.xmlgraphics.xmp.XMPConstants; +import org.apache.xmlgraphics.xmp.XMPSchema; +import org.apache.xmlgraphics.xmp.merge.MergeRuleSet; + +/** + * XMP Schema for PDF/A (ISO 19005-1). + */ +public class PDFAXMPSchema extends XMPSchema { + + /** Namespace URI for Dublin Core */ + public static final String NAMESPACE = XMPConstants.PDF_A_IDENTIFICATION; + + private static MergeRuleSet mergeRuleSet = new MergeRuleSet(); + + /** Creates a new schema instance for Dublin Core. */ + public PDFAXMPSchema() { + super(NAMESPACE, "pdfaid"); + } + + /** + * Creates and returns an adapter for this schema around the given metadata object. + * @param meta the metadata object + * @return the newly instantiated adapter + */ + public static PDFAAdapter getAdapter(Metadata meta) { + return new PDFAAdapter(meta, NAMESPACE); + } + + /** @see org.apache.xmlgraphics.xmp.XMPSchema#getDefaultMergeRuleSet() */ + public MergeRuleSet getDefaultMergeRuleSet() { + return mergeRuleSet; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/xmp/schemas/pdf/PDFUAAdapter.java b/src/main/java/org/apache/xmlgraphics/xmp/schemas/pdf/PDFUAAdapter.java new file mode 100644 index 0000000..610c309 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/xmp/schemas/pdf/PDFUAAdapter.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PDFUAAdapter.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.xmp.schemas.pdf; + +import org.apache.xmlgraphics.xmp.Metadata; +import org.apache.xmlgraphics.xmp.XMPSchemaAdapter; +import org.apache.xmlgraphics.xmp.XMPSchemaRegistry; + +/** + * Schema adapter implementation for both the old (deprecated) and the current PDF/UA schema. + * The old namespace is still needed to make Adobe Acrobat happy. + */ +public class PDFUAAdapter extends XMPSchemaAdapter { + + private static final String PART = "part"; + + /** + * Constructs a new adapter for PDF/UA around the given metadata object. + * @param meta the underlying metadata + * @param namespace the namespace to access the schema (must be one of the PDF/UA schema + * namespaces) + */ + public PDFUAAdapter(Metadata meta, String namespace) { + super(meta, XMPSchemaRegistry.getInstance().getSchema(namespace)); + } + + public void setPart(int value) { + setValue(PART, Integer.toString(value)); + } + + public int getPart() { + return Integer.parseInt(getValue(PART)); + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/xmp/schemas/pdf/PDFUAXMPSchema.java b/src/main/java/org/apache/xmlgraphics/xmp/schemas/pdf/PDFUAXMPSchema.java new file mode 100644 index 0000000..de4cf15 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/xmp/schemas/pdf/PDFUAXMPSchema.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PDFUAXMPSchema.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.xmp.schemas.pdf; + +import org.apache.xmlgraphics.xmp.Metadata; +import org.apache.xmlgraphics.xmp.XMPConstants; +import org.apache.xmlgraphics.xmp.XMPSchema; +import org.apache.xmlgraphics.xmp.merge.MergeRuleSet; + +/** + * XMP Schema for PDF/UA + */ +public class PDFUAXMPSchema extends XMPSchema { + + /** Namespace URI for Dublin Core */ + public static final String NAMESPACE = XMPConstants.PDF_UA_IDENTIFICATION; + + private static MergeRuleSet mergeRuleSet = new MergeRuleSet(); + + /** Creates a new schema instance for Dublin Core. */ + public PDFUAXMPSchema() { + super(NAMESPACE, "pdfuaid"); + } + + /** + * Creates and returns an adapter for this schema around the given metadata object. + * @param meta the metadata object + * @return the newly instantiated adapter + */ + public static PDFUAAdapter getAdapter(Metadata meta) { + return new PDFUAAdapter(meta, NAMESPACE); + } + + /** @see org.apache.xmlgraphics.xmp.XMPSchema#getDefaultMergeRuleSet() */ + public MergeRuleSet getDefaultMergeRuleSet() { + return mergeRuleSet; + } + +} diff --git a/src/main/java/org/apache/xmlgraphics/xmp/schemas/pdf/PDFVTAdapter.java b/src/main/java/org/apache/xmlgraphics/xmp/schemas/pdf/PDFVTAdapter.java new file mode 100644 index 0000000..aa53787 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/xmp/schemas/pdf/PDFVTAdapter.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PDFVTAdapter.java 1732018 2016-02-24 04:51:06Z gadams $ */ +package org.apache.xmlgraphics.xmp.schemas.pdf; + +import java.util.Date; + +import org.apache.xmlgraphics.xmp.Metadata; +import org.apache.xmlgraphics.xmp.XMPSchemaAdapter; +import org.apache.xmlgraphics.xmp.XMPSchemaRegistry; + +public class PDFVTAdapter extends XMPSchemaAdapter { + /** + * Constructs a new adapter for PDF/VT around the given metadata object. + * @param meta the underlying metadata + * @param namespace the namespace to access the schema (must be one of the PDF/VT schema + * namespaces) + */ + public PDFVTAdapter(Metadata meta, String namespace) { + super(meta, XMPSchemaRegistry.getInstance().getSchema(namespace)); + } + + public void setVersion(String v) { + setValue("GTS_PDFVTVersion", v); + } + + public void setModifyDate(Date modifyDate) { + setDateValue("GTS_PDFVTModDate", modifyDate); + } +} diff --git a/src/main/java/org/apache/xmlgraphics/xmp/schemas/pdf/PDFVTXMPSchema.java b/src/main/java/org/apache/xmlgraphics/xmp/schemas/pdf/PDFVTXMPSchema.java new file mode 100644 index 0000000..0bc8ed9 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/xmp/schemas/pdf/PDFVTXMPSchema.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PDFVTXMPSchema.java 1732018 2016-02-24 04:51:06Z gadams $ */ +package org.apache.xmlgraphics.xmp.schemas.pdf; + +import org.apache.xmlgraphics.xmp.Metadata; +import org.apache.xmlgraphics.xmp.XMPConstants; +import org.apache.xmlgraphics.xmp.XMPSchema; + +public class PDFVTXMPSchema extends XMPSchema { + public static final String NAMESPACE = XMPConstants.PDF_VT_IDENTIFICATION; + + /** Creates a new schema instance for Dublin Core. */ + public PDFVTXMPSchema() { + super(NAMESPACE, "pdfvtid"); + } + + /** + * Creates and returns an adapter for this schema around the given metadata object. + * @param meta the metadata object + * @return the newly instantiated adapter + */ + public static PDFVTAdapter getAdapter(Metadata meta) { + return new PDFVTAdapter(meta, NAMESPACE); + } +} diff --git a/src/main/java/org/apache/xmlgraphics/xmp/schemas/pdf/PDFXAdapter.java b/src/main/java/org/apache/xmlgraphics/xmp/schemas/pdf/PDFXAdapter.java new file mode 100644 index 0000000..93f7321 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/xmp/schemas/pdf/PDFXAdapter.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PDFXAdapter.java 1732018 2016-02-24 04:51:06Z gadams $ */ +package org.apache.xmlgraphics.xmp.schemas.pdf; + +import org.apache.xmlgraphics.xmp.Metadata; +import org.apache.xmlgraphics.xmp.XMPSchemaAdapter; +import org.apache.xmlgraphics.xmp.XMPSchemaRegistry; + +public class PDFXAdapter extends XMPSchemaAdapter { + /** + * Constructs a new adapter for PDF/X around the given metadata object. + * @param meta the underlying metadata + * @param namespace the namespace to access the schema (must be one of the PDF/X schema + * namespaces) + */ + public PDFXAdapter(Metadata meta, String namespace) { + super(meta, XMPSchemaRegistry.getInstance().getSchema(namespace)); + } + + public void setVersion(String v) { + setValue("GTS_PDFXVersion", v); + } +} diff --git a/src/main/java/org/apache/xmlgraphics/xmp/schemas/pdf/PDFXXMPSchema.java b/src/main/java/org/apache/xmlgraphics/xmp/schemas/pdf/PDFXXMPSchema.java new file mode 100644 index 0000000..d05fb3f --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/xmp/schemas/pdf/PDFXXMPSchema.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PDFXXMPSchema.java 1732018 2016-02-24 04:51:06Z gadams $ */ +package org.apache.xmlgraphics.xmp.schemas.pdf; + + +import org.apache.xmlgraphics.xmp.Metadata; +import org.apache.xmlgraphics.xmp.XMPConstants; +import org.apache.xmlgraphics.xmp.XMPSchema; + +public class PDFXXMPSchema extends XMPSchema { + public static final String NAMESPACE = XMPConstants.PDF_X_IDENTIFICATION; + + /** Creates a new schema instance for Dublin Core. */ + public PDFXXMPSchema() { + super(NAMESPACE, "pdfxid"); + } + + /** + * Creates and returns an adapter for this schema around the given metadata object. + * @param meta the metadata object + * @return the newly instantiated adapter + */ + public static PDFXAdapter getAdapter(Metadata meta) { + return new PDFXAdapter(meta, NAMESPACE); + } +} diff --git a/src/main/java/org/apache/xmlgraphics/xmp/schemas/pdf/XAPMMAdapter.java b/src/main/java/org/apache/xmlgraphics/xmp/schemas/pdf/XAPMMAdapter.java new file mode 100644 index 0000000..a50d38d --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/xmp/schemas/pdf/XAPMMAdapter.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: XAPMMAdapter.java 1732018 2016-02-24 04:51:06Z gadams $ */ +package org.apache.xmlgraphics.xmp.schemas.pdf; + +import org.apache.xmlgraphics.xmp.Metadata; +import org.apache.xmlgraphics.xmp.XMPSchemaAdapter; +import org.apache.xmlgraphics.xmp.XMPSchemaRegistry; + +public class XAPMMAdapter extends XMPSchemaAdapter { + /** + * Constructs a new adapter for XAP MM around the given metadata object. + * @param meta the underlying metadata + * @param namespace the namespace to access the schema (must be one of the PDF/VT schema + * namespaces) + */ + public XAPMMAdapter(Metadata meta, String namespace) { + super(meta, XMPSchemaRegistry.getInstance().getSchema(namespace)); + } + + public void setVersion(String v) { + setValue("VersionID", v); + } + + /** + * The rendition class name for this resource. + * @param c the value + */ + public void setRenditionClass(String c) { + setValue("RenditionClass", c); + } + + public void setInstanceID(String v) { + setValue("InstanceID", v); + } + + public void setDocumentID(String v) { + setValue("DocumentID", v); + } +} diff --git a/src/main/java/org/apache/xmlgraphics/xmp/schemas/pdf/XAPMMXMPSchema.java b/src/main/java/org/apache/xmlgraphics/xmp/schemas/pdf/XAPMMXMPSchema.java new file mode 100644 index 0000000..b7afc6c --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/xmp/schemas/pdf/XAPMMXMPSchema.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: XAPMMXMPSchema.java 1732018 2016-02-24 04:51:06Z gadams $ */ +package org.apache.xmlgraphics.xmp.schemas.pdf; + +import org.apache.xmlgraphics.xmp.Metadata; +import org.apache.xmlgraphics.xmp.XMPConstants; +import org.apache.xmlgraphics.xmp.XMPSchema; + +public class XAPMMXMPSchema extends XMPSchema { + public static final String NAMESPACE = XMPConstants.XAP_MM_NAMESPACE; + + /** Creates a new schema instance for Dublin Core. */ + public XAPMMXMPSchema() { + super(NAMESPACE, "xmpMM"); + } + + /** + * Creates and returns an adapter for this schema around the given metadata object. + * + * @param meta the metadata object + * @return the newly instantiated adapter + */ + public static XAPMMAdapter getAdapter(Metadata meta) { + return new XAPMMAdapter(meta, NAMESPACE); + } +} diff --git a/src/main/java/org/apache/xmlgraphics/xmp/schemas/pdf/package.html b/src/main/java/org/apache/xmlgraphics/xmp/schemas/pdf/package.html new file mode 100644 index 0000000..e11cca9 --- /dev/null +++ b/src/main/java/org/apache/xmlgraphics/xmp/schemas/pdf/package.html @@ -0,0 +1,23 @@ + + + +org.apache.xmlgraphics.commons.xmp.schemas.pdf Package + +

    PDF-specific XMP schemas and schema adapter classes.

    + + \ No newline at end of file diff --git a/src/main/resources/META-INF/services/javax.xml.transform.URIResolver b/src/main/resources/META-INF/services/javax.xml.transform.URIResolver new file mode 100644 index 0000000..9202f61 --- /dev/null +++ b/src/main/resources/META-INF/services/javax.xml.transform.URIResolver @@ -0,0 +1 @@ +org.apache.xmlgraphics.util.uri.DataURIResolver diff --git a/src/main/resources/META-INF/services/org.apache.xmlgraphics.image.loader.spi.ImageConverter b/src/main/resources/META-INF/services/org.apache.xmlgraphics.image.loader.spi.ImageConverter new file mode 100644 index 0000000..24b0d8b --- /dev/null +++ b/src/main/resources/META-INF/services/org.apache.xmlgraphics.image.loader.spi.ImageConverter @@ -0,0 +1,4 @@ +org.apache.xmlgraphics.image.loader.impl.ImageConverterBuffered2Rendered +org.apache.xmlgraphics.image.loader.impl.ImageConverterG2D2Bitmap +org.apache.xmlgraphics.image.loader.impl.ImageConverterBitmap2G2D +org.apache.xmlgraphics.image.loader.impl.ImageConverterRendered2PNG diff --git a/src/main/resources/META-INF/services/org.apache.xmlgraphics.image.loader.spi.ImageLoaderFactory b/src/main/resources/META-INF/services/org.apache.xmlgraphics.image.loader.spi.ImageLoaderFactory new file mode 100644 index 0000000..e1efed2 --- /dev/null +++ b/src/main/resources/META-INF/services/org.apache.xmlgraphics.image.loader.spi.ImageLoaderFactory @@ -0,0 +1,6 @@ +org.apache.xmlgraphics.image.loader.impl.imageio.ImageLoaderFactoryImageIO +org.apache.xmlgraphics.image.loader.impl.ImageLoaderFactoryRaw +org.apache.xmlgraphics.image.loader.impl.ImageLoaderFactoryRawCCITTFax +org.apache.xmlgraphics.image.loader.impl.ImageLoaderFactoryEPS +org.apache.xmlgraphics.image.loader.impl.ImageLoaderFactoryInternalTIFF +org.apache.xmlgraphics.image.loader.impl.ImageLoaderFactoryPNG diff --git a/src/main/resources/META-INF/services/org.apache.xmlgraphics.image.loader.spi.ImagePreloader b/src/main/resources/META-INF/services/org.apache.xmlgraphics.image.loader.spi.ImagePreloader new file mode 100644 index 0000000..e2c8c00 --- /dev/null +++ b/src/main/resources/META-INF/services/org.apache.xmlgraphics.image.loader.spi.ImagePreloader @@ -0,0 +1,8 @@ +org.apache.xmlgraphics.image.loader.impl.PreloaderTIFF +org.apache.xmlgraphics.image.loader.impl.PreloaderGIF +org.apache.xmlgraphics.image.loader.impl.PreloaderJPEG +org.apache.xmlgraphics.image.loader.impl.PreloaderBMP +org.apache.xmlgraphics.image.loader.impl.PreloaderEMF +org.apache.xmlgraphics.image.loader.impl.PreloaderEPS +org.apache.xmlgraphics.image.loader.impl.imageio.PreloaderImageIO +org.apache.xmlgraphics.image.loader.impl.PreloaderRawPNG \ No newline at end of file diff --git a/src/main/resources/META-INF/services/org.apache.xmlgraphics.image.writer.ImageWriter b/src/main/resources/META-INF/services/org.apache.xmlgraphics.image.writer.ImageWriter new file mode 100644 index 0000000..9f67d4a --- /dev/null +++ b/src/main/resources/META-INF/services/org.apache.xmlgraphics.image.writer.ImageWriter @@ -0,0 +1,5 @@ +org.apache.xmlgraphics.image.writer.internal.PNGImageWriter +org.apache.xmlgraphics.image.writer.internal.TIFFImageWriter +org.apache.xmlgraphics.image.writer.imageio.ImageIOPNGImageWriter +org.apache.xmlgraphics.image.writer.imageio.ImageIOTIFFImageWriter +org.apache.xmlgraphics.image.writer.imageio.ImageIOJPEGImageWriter diff --git a/src/main/resources/org/apache/xmlgraphics/fonts/glyphlist.txt b/src/main/resources/org/apache/xmlgraphics/fonts/glyphlist.txt new file mode 100644 index 0000000..0304ffc --- /dev/null +++ b/src/main/resources/org/apache/xmlgraphics/fonts/glyphlist.txt @@ -0,0 +1,4322 @@ +# ################################################################################### +# Copyright (c) 1997,1998,2002,2007 Adobe Systems Incorporated +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this documentation file to use, copy, publish, distribute, +# sublicense, and/or sell copies of the documentation, and to permit +# others to do the same, provided that: +# - No modification, editing or other alteration of this document is +# allowed; and +# - The above copyright notice and this permission notice shall be +# included in all copies of the documentation. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this documentation file, to create their own derivative works +# from the content of this document to use, copy, publish, distribute, +# sublicense, and/or sell the derivative works, and to permit others to do +# the same, provided that the derived work is not represented as being a +# copy or version of this document. +# +# Adobe shall not be liable to any party for any loss of revenue or profit +# or for indirect, incidental, special, consequential, or other similar +# damages, whether based on tort (including without limitation negligence +# or strict liability), contract or other legal or equitable grounds even +# if Adobe has been advised or had reason to know of the possibility of +# such damages. The Adobe materials are provided on an "AS IS" basis. +# Adobe specifically disclaims all express, statutory, or implied +# warranties relating to the Adobe materials, including but not limited to +# those concerning merchantability or fitness for a particular purpose or +# non-infringement of any third party rights regarding the Adobe +# materials. +# ################################################################################### +# Name: Adobe Glyph List +# Table version: 2.0 +# Date: September 20, 2002 +# +# See http://partners.adobe.com/asn/developer/typeforum/unicodegn.html +# +# Format: Semicolon-delimited fields: +# (1) glyph name +# (2) Unicode scalar value +A;0041 +AE;00C6 +AEacute;01FC +AEmacron;01E2 +AEsmall;F7E6 +Aacute;00C1 +Aacutesmall;F7E1 +Abreve;0102 +Abreveacute;1EAE +Abrevecyrillic;04D0 +Abrevedotbelow;1EB6 +Abrevegrave;1EB0 +Abrevehookabove;1EB2 +Abrevetilde;1EB4 +Acaron;01CD +Acircle;24B6 +Acircumflex;00C2 +Acircumflexacute;1EA4 +Acircumflexdotbelow;1EAC +Acircumflexgrave;1EA6 +Acircumflexhookabove;1EA8 +Acircumflexsmall;F7E2 +Acircumflextilde;1EAA +Acute;F6C9 +Acutesmall;F7B4 +Acyrillic;0410 +Adblgrave;0200 +Adieresis;00C4 +Adieresiscyrillic;04D2 +Adieresismacron;01DE +Adieresissmall;F7E4 +Adotbelow;1EA0 +Adotmacron;01E0 +Agrave;00C0 +Agravesmall;F7E0 +Ahookabove;1EA2 +Aiecyrillic;04D4 +Ainvertedbreve;0202 +Alpha;0391 +Alphatonos;0386 +Amacron;0100 +Amonospace;FF21 +Aogonek;0104 +Aring;00C5 +Aringacute;01FA +Aringbelow;1E00 +Aringsmall;F7E5 +Asmall;F761 +Atilde;00C3 +Atildesmall;F7E3 +Aybarmenian;0531 +B;0042 +Bcircle;24B7 +Bdotaccent;1E02 +Bdotbelow;1E04 +Becyrillic;0411 +Benarmenian;0532 +Beta;0392 +Bhook;0181 +Blinebelow;1E06 +Bmonospace;FF22 +Brevesmall;F6F4 +Bsmall;F762 +Btopbar;0182 +C;0043 +Caarmenian;053E +Cacute;0106 +Caron;F6CA +Caronsmall;F6F5 +Ccaron;010C +Ccedilla;00C7 +Ccedillaacute;1E08 +Ccedillasmall;F7E7 +Ccircle;24B8 +Ccircumflex;0108 +Cdot;010A +Cdotaccent;010A +Cedillasmall;F7B8 +Chaarmenian;0549 +Cheabkhasiancyrillic;04BC +Checyrillic;0427 +Chedescenderabkhasiancyrillic;04BE +Chedescendercyrillic;04B6 +Chedieresiscyrillic;04F4 +Cheharmenian;0543 +Chekhakassiancyrillic;04CB +Cheverticalstrokecyrillic;04B8 +Chi;03A7 +Chook;0187 +Circumflexsmall;F6F6 +Cmonospace;FF23 +Coarmenian;0551 +Csmall;F763 +D;0044 +DZ;01F1 +DZcaron;01C4 +Daarmenian;0534 +Dafrican;0189 +Dcaron;010E +Dcedilla;1E10 +Dcircle;24B9 +Dcircumflexbelow;1E12 +Dcroat;0110 +Ddotaccent;1E0A +Ddotbelow;1E0C +Decyrillic;0414 +Deicoptic;03EE +Delta;2206 +Deltagreek;0394 +Dhook;018A +Dieresis;F6CB +DieresisAcute;F6CC +DieresisGrave;F6CD +Dieresissmall;F7A8 +Digammagreek;03DC +Djecyrillic;0402 +Dlinebelow;1E0E +Dmonospace;FF24 +Dotaccentsmall;F6F7 +Dslash;0110 +Dsmall;F764 +Dtopbar;018B +Dz;01F2 +Dzcaron;01C5 +Dzeabkhasiancyrillic;04E0 +Dzecyrillic;0405 +Dzhecyrillic;040F +E;0045 +Eacute;00C9 +Eacutesmall;F7E9 +Ebreve;0114 +Ecaron;011A +Ecedillabreve;1E1C +Echarmenian;0535 +Ecircle;24BA +Ecircumflex;00CA +Ecircumflexacute;1EBE +Ecircumflexbelow;1E18 +Ecircumflexdotbelow;1EC6 +Ecircumflexgrave;1EC0 +Ecircumflexhookabove;1EC2 +Ecircumflexsmall;F7EA +Ecircumflextilde;1EC4 +Ecyrillic;0404 +Edblgrave;0204 +Edieresis;00CB +Edieresissmall;F7EB +Edot;0116 +Edotaccent;0116 +Edotbelow;1EB8 +Efcyrillic;0424 +Egrave;00C8 +Egravesmall;F7E8 +Eharmenian;0537 +Ehookabove;1EBA +Eightroman;2167 +Einvertedbreve;0206 +Eiotifiedcyrillic;0464 +Elcyrillic;041B +Elevenroman;216A +Emacron;0112 +Emacronacute;1E16 +Emacrongrave;1E14 +Emcyrillic;041C +Emonospace;FF25 +Encyrillic;041D +Endescendercyrillic;04A2 +Eng;014A +Enghecyrillic;04A4 +Enhookcyrillic;04C7 +Eogonek;0118 +Eopen;0190 +Epsilon;0395 +Epsilontonos;0388 +Ercyrillic;0420 +Ereversed;018E +Ereversedcyrillic;042D +Escyrillic;0421 +Esdescendercyrillic;04AA +Esh;01A9 +Esmall;F765 +Eta;0397 +Etarmenian;0538 +Etatonos;0389 +Eth;00D0 +Ethsmall;F7F0 +Etilde;1EBC +Etildebelow;1E1A +Euro;20AC +Ezh;01B7 +Ezhcaron;01EE +Ezhreversed;01B8 +F;0046 +Fcircle;24BB +Fdotaccent;1E1E +Feharmenian;0556 +Feicoptic;03E4 +Fhook;0191 +Fitacyrillic;0472 +Fiveroman;2164 +Fmonospace;FF26 +Fourroman;2163 +Fsmall;F766 +G;0047 +GBsquare;3387 +Gacute;01F4 +Gamma;0393 +Gammaafrican;0194 +Gangiacoptic;03EA +Gbreve;011E +Gcaron;01E6 +Gcedilla;0122 +Gcircle;24BC +Gcircumflex;011C +Gcommaaccent;0122 +Gdot;0120 +Gdotaccent;0120 +Gecyrillic;0413 +Ghadarmenian;0542 +Ghemiddlehookcyrillic;0494 +Ghestrokecyrillic;0492 +Gheupturncyrillic;0490 +Ghook;0193 +Gimarmenian;0533 +Gjecyrillic;0403 +Gmacron;1E20 +Gmonospace;FF27 +Grave;F6CE +Gravesmall;F760 +Gsmall;F767 +Gsmallhook;029B +Gstroke;01E4 +H;0048 +H18533;25CF +H18543;25AA +H18551;25AB +H22073;25A1 +HPsquare;33CB +Haabkhasiancyrillic;04A8 +Hadescendercyrillic;04B2 +Hardsigncyrillic;042A +Hbar;0126 +Hbrevebelow;1E2A +Hcedilla;1E28 +Hcircle;24BD +Hcircumflex;0124 +Hdieresis;1E26 +Hdotaccent;1E22 +Hdotbelow;1E24 +Hmonospace;FF28 +Hoarmenian;0540 +Horicoptic;03E8 +Hsmall;F768 +Hungarumlaut;F6CF +Hungarumlautsmall;F6F8 +Hzsquare;3390 +I;0049 +IAcyrillic;042F +IJ;0132 +IUcyrillic;042E +Iacute;00CD +Iacutesmall;F7ED +Ibreve;012C +Icaron;01CF +Icircle;24BE +Icircumflex;00CE +Icircumflexsmall;F7EE +Icyrillic;0406 +Idblgrave;0208 +Idieresis;00CF +Idieresisacute;1E2E +Idieresiscyrillic;04E4 +Idieresissmall;F7EF +Idot;0130 +Idotaccent;0130 +Idotbelow;1ECA +Iebrevecyrillic;04D6 +Iecyrillic;0415 +Ifraktur;2111 +Igrave;00CC +Igravesmall;F7EC +Ihookabove;1EC8 +Iicyrillic;0418 +Iinvertedbreve;020A +Iishortcyrillic;0419 +Imacron;012A +Imacroncyrillic;04E2 +Imonospace;FF29 +Iniarmenian;053B +Iocyrillic;0401 +Iogonek;012E +Iota;0399 +Iotaafrican;0196 +Iotadieresis;03AA +Iotatonos;038A +Ismall;F769 +Istroke;0197 +Itilde;0128 +Itildebelow;1E2C +Izhitsacyrillic;0474 +Izhitsadblgravecyrillic;0476 +J;004A +Jaarmenian;0541 +Jcircle;24BF +Jcircumflex;0134 +Jecyrillic;0408 +Jheharmenian;054B +Jmonospace;FF2A +Jsmall;F76A +K;004B +KBsquare;3385 +KKsquare;33CD +Kabashkircyrillic;04A0 +Kacute;1E30 +Kacyrillic;041A +Kadescendercyrillic;049A +Kahookcyrillic;04C3 +Kappa;039A +Kastrokecyrillic;049E +Kaverticalstrokecyrillic;049C +Kcaron;01E8 +Kcedilla;0136 +Kcircle;24C0 +Kcommaaccent;0136 +Kdotbelow;1E32 +Keharmenian;0554 +Kenarmenian;053F +Khacyrillic;0425 +Kheicoptic;03E6 +Khook;0198 +Kjecyrillic;040C +Klinebelow;1E34 +Kmonospace;FF2B +Koppacyrillic;0480 +Koppagreek;03DE +Ksicyrillic;046E +Ksmall;F76B +L;004C +LJ;01C7 +LL;F6BF +Lacute;0139 +Lambda;039B +Lcaron;013D +Lcedilla;013B +Lcircle;24C1 +Lcircumflexbelow;1E3C +Lcommaaccent;013B +Ldot;013F +Ldotaccent;013F +Ldotbelow;1E36 +Ldotbelowmacron;1E38 +Liwnarmenian;053C +Lj;01C8 +Ljecyrillic;0409 +Llinebelow;1E3A +Lmonospace;FF2C +Lslash;0141 +Lslashsmall;F6F9 +Lsmall;F76C +M;004D +MBsquare;3386 +Macron;F6D0 +Macronsmall;F7AF +Macute;1E3E +Mcircle;24C2 +Mdotaccent;1E40 +Mdotbelow;1E42 +Menarmenian;0544 +Mmonospace;FF2D +Msmall;F76D +Mturned;019C +Mu;039C +N;004E +NJ;01CA +Nacute;0143 +Ncaron;0147 +Ncedilla;0145 +Ncircle;24C3 +Ncircumflexbelow;1E4A +Ncommaaccent;0145 +Ndotaccent;1E44 +Ndotbelow;1E46 +Nhookleft;019D +Nineroman;2168 +Nj;01CB +Njecyrillic;040A +Nlinebelow;1E48 +Nmonospace;FF2E +Nowarmenian;0546 +Nsmall;F76E +Ntilde;00D1 +Ntildesmall;F7F1 +Nu;039D +O;004F +OE;0152 +OEsmall;F6FA +Oacute;00D3 +Oacutesmall;F7F3 +Obarredcyrillic;04E8 +Obarreddieresiscyrillic;04EA +Obreve;014E +Ocaron;01D1 +Ocenteredtilde;019F +Ocircle;24C4 +Ocircumflex;00D4 +Ocircumflexacute;1ED0 +Ocircumflexdotbelow;1ED8 +Ocircumflexgrave;1ED2 +Ocircumflexhookabove;1ED4 +Ocircumflexsmall;F7F4 +Ocircumflextilde;1ED6 +Ocyrillic;041E +Odblacute;0150 +Odblgrave;020C +Odieresis;00D6 +Odieresiscyrillic;04E6 +Odieresissmall;F7F6 +Odotbelow;1ECC +Ogoneksmall;F6FB +Ograve;00D2 +Ogravesmall;F7F2 +Oharmenian;0555 +Ohm;2126 +Ohookabove;1ECE +Ohorn;01A0 +Ohornacute;1EDA +Ohorndotbelow;1EE2 +Ohorngrave;1EDC +Ohornhookabove;1EDE +Ohorntilde;1EE0 +Ohungarumlaut;0150 +Oi;01A2 +Oinvertedbreve;020E +Omacron;014C +Omacronacute;1E52 +Omacrongrave;1E50 +Omega;2126 +Omegacyrillic;0460 +Omegagreek;03A9 +Omegaroundcyrillic;047A +Omegatitlocyrillic;047C +Omegatonos;038F +Omicron;039F +Omicrontonos;038C +Omonospace;FF2F +Oneroman;2160 +Oogonek;01EA +Oogonekmacron;01EC +Oopen;0186 +Oslash;00D8 +Oslashacute;01FE +Oslashsmall;F7F8 +Osmall;F76F +Ostrokeacute;01FE +Otcyrillic;047E +Otilde;00D5 +Otildeacute;1E4C +Otildedieresis;1E4E +Otildesmall;F7F5 +P;0050 +Pacute;1E54 +Pcircle;24C5 +Pdotaccent;1E56 +Pecyrillic;041F +Peharmenian;054A +Pemiddlehookcyrillic;04A6 +Phi;03A6 +Phook;01A4 +Pi;03A0 +Piwrarmenian;0553 +Pmonospace;FF30 +Psi;03A8 +Psicyrillic;0470 +Psmall;F770 +Q;0051 +Qcircle;24C6 +Qmonospace;FF31 +Qsmall;F771 +R;0052 +Raarmenian;054C +Racute;0154 +Rcaron;0158 +Rcedilla;0156 +Rcircle;24C7 +Rcommaaccent;0156 +Rdblgrave;0210 +Rdotaccent;1E58 +Rdotbelow;1E5A +Rdotbelowmacron;1E5C +Reharmenian;0550 +Rfraktur;211C +Rho;03A1 +Ringsmall;F6FC +Rinvertedbreve;0212 +Rlinebelow;1E5E +Rmonospace;FF32 +Rsmall;F772 +Rsmallinverted;0281 +Rsmallinvertedsuperior;02B6 +S;0053 +SF010000;250C +SF020000;2514 +SF030000;2510 +SF040000;2518 +SF050000;253C +SF060000;252C +SF070000;2534 +SF080000;251C +SF090000;2524 +SF100000;2500 +SF110000;2502 +SF190000;2561 +SF200000;2562 +SF210000;2556 +SF220000;2555 +SF230000;2563 +SF240000;2551 +SF250000;2557 +SF260000;255D +SF270000;255C +SF280000;255B +SF360000;255E +SF370000;255F +SF380000;255A +SF390000;2554 +SF400000;2569 +SF410000;2566 +SF420000;2560 +SF430000;2550 +SF440000;256C +SF450000;2567 +SF460000;2568 +SF470000;2564 +SF480000;2565 +SF490000;2559 +SF500000;2558 +SF510000;2552 +SF520000;2553 +SF530000;256B +SF540000;256A +Sacute;015A +Sacutedotaccent;1E64 +Sampigreek;03E0 +Scaron;0160 +Scarondotaccent;1E66 +Scaronsmall;F6FD +Scedilla;015E +Schwa;018F +Schwacyrillic;04D8 +Schwadieresiscyrillic;04DA +Scircle;24C8 +Scircumflex;015C +Scommaaccent;0218 +Sdotaccent;1E60 +Sdotbelow;1E62 +Sdotbelowdotaccent;1E68 +Seharmenian;054D +Sevenroman;2166 +Shaarmenian;0547 +Shacyrillic;0428 +Shchacyrillic;0429 +Sheicoptic;03E2 +Shhacyrillic;04BA +Shimacoptic;03EC +Sigma;03A3 +Sixroman;2165 +Smonospace;FF33 +Softsigncyrillic;042C +Ssmall;F773 +Stigmagreek;03DA +T;0054 +Tau;03A4 +Tbar;0166 +Tcaron;0164 +Tcedilla;0162 +Tcircle;24C9 +Tcircumflexbelow;1E70 +Tcommaaccent;0162 +Tdotaccent;1E6A +Tdotbelow;1E6C +Tecyrillic;0422 +Tedescendercyrillic;04AC +Tenroman;2169 +Tetsecyrillic;04B4 +Theta;0398 +Thook;01AC +Thorn;00DE +Thornsmall;F7FE +Threeroman;2162 +Tildesmall;F6FE +Tiwnarmenian;054F +Tlinebelow;1E6E +Tmonospace;FF34 +Toarmenian;0539 +Tonefive;01BC +Tonesix;0184 +Tonetwo;01A7 +Tretroflexhook;01AE +Tsecyrillic;0426 +Tshecyrillic;040B +Tsmall;F774 +Twelveroman;216B +Tworoman;2161 +U;0055 +Uacute;00DA +Uacutesmall;F7FA +Ubreve;016C +Ucaron;01D3 +Ucircle;24CA +Ucircumflex;00DB +Ucircumflexbelow;1E76 +Ucircumflexsmall;F7FB +Ucyrillic;0423 +Udblacute;0170 +Udblgrave;0214 +Udieresis;00DC +Udieresisacute;01D7 +Udieresisbelow;1E72 +Udieresiscaron;01D9 +Udieresiscyrillic;04F0 +Udieresisgrave;01DB +Udieresismacron;01D5 +Udieresissmall;F7FC +Udotbelow;1EE4 +Ugrave;00D9 +Ugravesmall;F7F9 +Uhookabove;1EE6 +Uhorn;01AF +Uhornacute;1EE8 +Uhorndotbelow;1EF0 +Uhorngrave;1EEA +Uhornhookabove;1EEC +Uhorntilde;1EEE +Uhungarumlaut;0170 +Uhungarumlautcyrillic;04F2 +Uinvertedbreve;0216 +Ukcyrillic;0478 +Umacron;016A +Umacroncyrillic;04EE +Umacrondieresis;1E7A +Umonospace;FF35 +Uogonek;0172 +Upsilon;03A5 +Upsilon1;03D2 +Upsilonacutehooksymbolgreek;03D3 +Upsilonafrican;01B1 +Upsilondieresis;03AB +Upsilondieresishooksymbolgreek;03D4 +Upsilonhooksymbol;03D2 +Upsilontonos;038E +Uring;016E +Ushortcyrillic;040E +Usmall;F775 +Ustraightcyrillic;04AE +Ustraightstrokecyrillic;04B0 +Utilde;0168 +Utildeacute;1E78 +Utildebelow;1E74 +V;0056 +Vcircle;24CB +Vdotbelow;1E7E +Vecyrillic;0412 +Vewarmenian;054E +Vhook;01B2 +Vmonospace;FF36 +Voarmenian;0548 +Vsmall;F776 +Vtilde;1E7C +W;0057 +Wacute;1E82 +Wcircle;24CC +Wcircumflex;0174 +Wdieresis;1E84 +Wdotaccent;1E86 +Wdotbelow;1E88 +Wgrave;1E80 +Wmonospace;FF37 +Wsmall;F777 +X;0058 +Xcircle;24CD +Xdieresis;1E8C +Xdotaccent;1E8A +Xeharmenian;053D +Xi;039E +Xmonospace;FF38 +Xsmall;F778 +Y;0059 +Yacute;00DD +Yacutesmall;F7FD +Yatcyrillic;0462 +Ycircle;24CE +Ycircumflex;0176 +Ydieresis;0178 +Ydieresissmall;F7FF +Ydotaccent;1E8E +Ydotbelow;1EF4 +Yericyrillic;042B +Yerudieresiscyrillic;04F8 +Ygrave;1EF2 +Yhook;01B3 +Yhookabove;1EF6 +Yiarmenian;0545 +Yicyrillic;0407 +Yiwnarmenian;0552 +Ymonospace;FF39 +Ysmall;F779 +Ytilde;1EF8 +Yusbigcyrillic;046A +Yusbigiotifiedcyrillic;046C +Yuslittlecyrillic;0466 +Yuslittleiotifiedcyrillic;0468 +Z;005A +Zaarmenian;0536 +Zacute;0179 +Zcaron;017D +Zcaronsmall;F6FF +Zcircle;24CF +Zcircumflex;1E90 +Zdot;017B +Zdotaccent;017B +Zdotbelow;1E92 +Zecyrillic;0417 +Zedescendercyrillic;0498 +Zedieresiscyrillic;04DE +Zeta;0396 +Zhearmenian;053A +Zhebrevecyrillic;04C1 +Zhecyrillic;0416 +Zhedescendercyrillic;0496 +Zhedieresiscyrillic;04DC +Zlinebelow;1E94 +Zmonospace;FF3A +Zsmall;F77A +Zstroke;01B5 +a;0061 +aabengali;0986 +aacute;00E1 +aadeva;0906 +aagujarati;0A86 +aagurmukhi;0A06 +aamatragurmukhi;0A3E +aarusquare;3303 +aavowelsignbengali;09BE +aavowelsigndeva;093E +aavowelsigngujarati;0ABE +abbreviationmarkarmenian;055F +abbreviationsigndeva;0970 +abengali;0985 +abopomofo;311A +abreve;0103 +abreveacute;1EAF +abrevecyrillic;04D1 +abrevedotbelow;1EB7 +abrevegrave;1EB1 +abrevehookabove;1EB3 +abrevetilde;1EB5 +acaron;01CE +acircle;24D0 +acircumflex;00E2 +acircumflexacute;1EA5 +acircumflexdotbelow;1EAD +acircumflexgrave;1EA7 +acircumflexhookabove;1EA9 +acircumflextilde;1EAB +acute;00B4 +acutebelowcmb;0317 +acutecmb;0301 +acutecomb;0301 +acutedeva;0954 +acutelowmod;02CF +acutetonecmb;0341 +acyrillic;0430 +adblgrave;0201 +addakgurmukhi;0A71 +adeva;0905 +adieresis;00E4 +adieresiscyrillic;04D3 +adieresismacron;01DF +adotbelow;1EA1 +adotmacron;01E1 +ae;00E6 +aeacute;01FD +aekorean;3150 +aemacron;01E3 +afii00208;2015 +afii08941;20A4 +afii10017;0410 +afii10018;0411 +afii10019;0412 +afii10020;0413 +afii10021;0414 +afii10022;0415 +afii10023;0401 +afii10024;0416 +afii10025;0417 +afii10026;0418 +afii10027;0419 +afii10028;041A +afii10029;041B +afii10030;041C +afii10031;041D +afii10032;041E +afii10033;041F +afii10034;0420 +afii10035;0421 +afii10036;0422 +afii10037;0423 +afii10038;0424 +afii10039;0425 +afii10040;0426 +afii10041;0427 +afii10042;0428 +afii10043;0429 +afii10044;042A +afii10045;042B +afii10046;042C +afii10047;042D +afii10048;042E +afii10049;042F +afii10050;0490 +afii10051;0402 +afii10052;0403 +afii10053;0404 +afii10054;0405 +afii10055;0406 +afii10056;0407 +afii10057;0408 +afii10058;0409 +afii10059;040A +afii10060;040B +afii10061;040C +afii10062;040E +afii10063;F6C4 +afii10064;F6C5 +afii10065;0430 +afii10066;0431 +afii10067;0432 +afii10068;0433 +afii10069;0434 +afii10070;0435 +afii10071;0451 +afii10072;0436 +afii10073;0437 +afii10074;0438 +afii10075;0439 +afii10076;043A +afii10077;043B +afii10078;043C +afii10079;043D +afii10080;043E +afii10081;043F +afii10082;0440 +afii10083;0441 +afii10084;0442 +afii10085;0443 +afii10086;0444 +afii10087;0445 +afii10088;0446 +afii10089;0447 +afii10090;0448 +afii10091;0449 +afii10092;044A +afii10093;044B +afii10094;044C +afii10095;044D +afii10096;044E +afii10097;044F +afii10098;0491 +afii10099;0452 +afii10100;0453 +afii10101;0454 +afii10102;0455 +afii10103;0456 +afii10104;0457 +afii10105;0458 +afii10106;0459 +afii10107;045A +afii10108;045B +afii10109;045C +afii10110;045E +afii10145;040F +afii10146;0462 +afii10147;0472 +afii10148;0474 +afii10192;F6C6 +afii10193;045F +afii10194;0463 +afii10195;0473 +afii10196;0475 +afii10831;F6C7 +afii10832;F6C8 +afii10846;04D9 +afii299;200E +afii300;200F +afii301;200D +afii57381;066A +afii57388;060C +afii57392;0660 +afii57393;0661 +afii57394;0662 +afii57395;0663 +afii57396;0664 +afii57397;0665 +afii57398;0666 +afii57399;0667 +afii57400;0668 +afii57401;0669 +afii57403;061B +afii57407;061F +afii57409;0621 +afii57410;0622 +afii57411;0623 +afii57412;0624 +afii57413;0625 +afii57414;0626 +afii57415;0627 +afii57416;0628 +afii57417;0629 +afii57418;062A +afii57419;062B +afii57420;062C +afii57421;062D +afii57422;062E +afii57423;062F +afii57424;0630 +afii57425;0631 +afii57426;0632 +afii57427;0633 +afii57428;0634 +afii57429;0635 +afii57430;0636 +afii57431;0637 +afii57432;0638 +afii57433;0639 +afii57434;063A +afii57440;0640 +afii57441;0641 +afii57442;0642 +afii57443;0643 +afii57444;0644 +afii57445;0645 +afii57446;0646 +afii57448;0648 +afii57449;0649 +afii57450;064A +afii57451;064B +afii57452;064C +afii57453;064D +afii57454;064E +afii57455;064F +afii57456;0650 +afii57457;0651 +afii57458;0652 +afii57470;0647 +afii57505;06A4 +afii57506;067E +afii57507;0686 +afii57508;0698 +afii57509;06AF +afii57511;0679 +afii57512;0688 +afii57513;0691 +afii57514;06BA +afii57519;06D2 +afii57534;06D5 +afii57636;20AA +afii57645;05BE +afii57658;05C3 +afii57664;05D0 +afii57665;05D1 +afii57666;05D2 +afii57667;05D3 +afii57668;05D4 +afii57669;05D5 +afii57670;05D6 +afii57671;05D7 +afii57672;05D8 +afii57673;05D9 +afii57674;05DA +afii57675;05DB +afii57676;05DC +afii57677;05DD +afii57678;05DE +afii57679;05DF +afii57680;05E0 +afii57681;05E1 +afii57682;05E2 +afii57683;05E3 +afii57684;05E4 +afii57685;05E5 +afii57686;05E6 +afii57687;05E7 +afii57688;05E8 +afii57689;05E9 +afii57690;05EA +afii57694;FB2A +afii57695;FB2B +afii57700;FB4B +afii57705;FB1F +afii57716;05F0 +afii57717;05F1 +afii57718;05F2 +afii57723;FB35 +afii57793;05B4 +afii57794;05B5 +afii57795;05B6 +afii57796;05BB +afii57797;05B8 +afii57798;05B7 +afii57799;05B0 +afii57800;05B2 +afii57801;05B1 +afii57802;05B3 +afii57803;05C2 +afii57804;05C1 +afii57806;05B9 +afii57807;05BC +afii57839;05BD +afii57841;05BF +afii57842;05C0 +afii57929;02BC +afii61248;2105 +afii61289;2113 +afii61352;2116 +afii61573;202C +afii61574;202D +afii61575;202E +afii61664;200C +afii63167;066D +afii64937;02BD +agrave;00E0 +agujarati;0A85 +agurmukhi;0A05 +ahiragana;3042 +ahookabove;1EA3 +aibengali;0990 +aibopomofo;311E +aideva;0910 +aiecyrillic;04D5 +aigujarati;0A90 +aigurmukhi;0A10 +aimatragurmukhi;0A48 +ainarabic;0639 +ainfinalarabic;FECA +aininitialarabic;FECB +ainmedialarabic;FECC +ainvertedbreve;0203 +aivowelsignbengali;09C8 +aivowelsigndeva;0948 +aivowelsigngujarati;0AC8 +akatakana;30A2 +akatakanahalfwidth;FF71 +akorean;314F +alef;05D0 +alefarabic;0627 +alefdageshhebrew;FB30 +aleffinalarabic;FE8E +alefhamzaabovearabic;0623 +alefhamzaabovefinalarabic;FE84 +alefhamzabelowarabic;0625 +alefhamzabelowfinalarabic;FE88 +alefhebrew;05D0 +aleflamedhebrew;FB4F +alefmaddaabovearabic;0622 +alefmaddaabovefinalarabic;FE82 +alefmaksuraarabic;0649 +alefmaksurafinalarabic;FEF0 +alefmaksurainitialarabic;FEF3 +alefmaksuramedialarabic;FEF4 +alefpatahhebrew;FB2E +alefqamatshebrew;FB2F +aleph;2135 +allequal;224C +alpha;03B1 +alphatonos;03AC +amacron;0101 +amonospace;FF41 +ampersand;0026 +ampersandmonospace;FF06 +ampersandsmall;F726 +amsquare;33C2 +anbopomofo;3122 +angbopomofo;3124 +angkhankhuthai;0E5A +angle;2220 +anglebracketleft;3008 +anglebracketleftvertical;FE3F +anglebracketright;3009 +anglebracketrightvertical;FE40 +angleleft;2329 +angleright;232A +angstrom;212B +anoteleia;0387 +anudattadeva;0952 +anusvarabengali;0982 +anusvaradeva;0902 +anusvaragujarati;0A82 +aogonek;0105 +apaatosquare;3300 +aparen;249C +apostrophearmenian;055A +apostrophemod;02BC +apple;F8FF +approaches;2250 +approxequal;2248 +approxequalorimage;2252 +approximatelyequal;2245 +araeaekorean;318E +araeakorean;318D +arc;2312 +arighthalfring;1E9A +aring;00E5 +aringacute;01FB +aringbelow;1E01 +arrowboth;2194 +arrowdashdown;21E3 +arrowdashleft;21E0 +arrowdashright;21E2 +arrowdashup;21E1 +arrowdblboth;21D4 +arrowdbldown;21D3 +arrowdblleft;21D0 +arrowdblright;21D2 +arrowdblup;21D1 +arrowdown;2193 +arrowdownleft;2199 +arrowdownright;2198 +arrowdownwhite;21E9 +arrowheaddownmod;02C5 +arrowheadleftmod;02C2 +arrowheadrightmod;02C3 +arrowheadupmod;02C4 +arrowhorizex;F8E7 +arrowleft;2190 +arrowleftdbl;21D0 +arrowleftdblstroke;21CD +arrowleftoverright;21C6 +arrowleftwhite;21E6 +arrowright;2192 +arrowrightdblstroke;21CF +arrowrightheavy;279E +arrowrightoverleft;21C4 +arrowrightwhite;21E8 +arrowtableft;21E4 +arrowtabright;21E5 +arrowup;2191 +arrowupdn;2195 +arrowupdnbse;21A8 +arrowupdownbase;21A8 +arrowupleft;2196 +arrowupleftofdown;21C5 +arrowupright;2197 +arrowupwhite;21E7 +arrowvertex;F8E6 +asciicircum;005E +asciicircummonospace;FF3E +asciitilde;007E +asciitildemonospace;FF5E +ascript;0251 +ascriptturned;0252 +asmallhiragana;3041 +asmallkatakana;30A1 +asmallkatakanahalfwidth;FF67 +asterisk;002A +asteriskaltonearabic;066D +asteriskarabic;066D +asteriskmath;2217 +asteriskmonospace;FF0A +asterisksmall;FE61 +asterism;2042 +asuperior;F6E9 +asymptoticallyequal;2243 +at;0040 +atilde;00E3 +atmonospace;FF20 +atsmall;FE6B +aturned;0250 +aubengali;0994 +aubopomofo;3120 +audeva;0914 +augujarati;0A94 +augurmukhi;0A14 +aulengthmarkbengali;09D7 +aumatragurmukhi;0A4C +auvowelsignbengali;09CC +auvowelsigndeva;094C +auvowelsigngujarati;0ACC +avagrahadeva;093D +aybarmenian;0561 +ayin;05E2 +ayinaltonehebrew;FB20 +ayinhebrew;05E2 +b;0062 +babengali;09AC +backslash;005C +backslashmonospace;FF3C +badeva;092C +bagujarati;0AAC +bagurmukhi;0A2C +bahiragana;3070 +bahtthai;0E3F +bakatakana;30D0 +bar;007C +barmonospace;FF5C +bbopomofo;3105 +bcircle;24D1 +bdotaccent;1E03 +bdotbelow;1E05 +beamedsixteenthnotes;266C +because;2235 +becyrillic;0431 +beharabic;0628 +behfinalarabic;FE90 +behinitialarabic;FE91 +behiragana;3079 +behmedialarabic;FE92 +behmeeminitialarabic;FC9F +behmeemisolatedarabic;FC08 +behnoonfinalarabic;FC6D +bekatakana;30D9 +benarmenian;0562 +bet;05D1 +beta;03B2 +betasymbolgreek;03D0 +betdagesh;FB31 +betdageshhebrew;FB31 +bethebrew;05D1 +betrafehebrew;FB4C +bhabengali;09AD +bhadeva;092D +bhagujarati;0AAD +bhagurmukhi;0A2D +bhook;0253 +bihiragana;3073 +bikatakana;30D3 +bilabialclick;0298 +bindigurmukhi;0A02 +birusquare;3331 +blackcircle;25CF +blackdiamond;25C6 +blackdownpointingtriangle;25BC +blackleftpointingpointer;25C4 +blackleftpointingtriangle;25C0 +blacklenticularbracketleft;3010 +blacklenticularbracketleftvertical;FE3B +blacklenticularbracketright;3011 +blacklenticularbracketrightvertical;FE3C +blacklowerlefttriangle;25E3 +blacklowerrighttriangle;25E2 +blackrectangle;25AC +blackrightpointingpointer;25BA +blackrightpointingtriangle;25B6 +blacksmallsquare;25AA +blacksmilingface;263B +blacksquare;25A0 +blackstar;2605 +blackupperlefttriangle;25E4 +blackupperrighttriangle;25E5 +blackuppointingsmalltriangle;25B4 +blackuppointingtriangle;25B2 +blank;2423 +blinebelow;1E07 +block;2588 +bmonospace;FF42 +bobaimaithai;0E1A +bohiragana;307C +bokatakana;30DC +bparen;249D +bqsquare;33C3 +braceex;F8F4 +braceleft;007B +braceleftbt;F8F3 +braceleftmid;F8F2 +braceleftmonospace;FF5B +braceleftsmall;FE5B +bracelefttp;F8F1 +braceleftvertical;FE37 +braceright;007D +bracerightbt;F8FE +bracerightmid;F8FD +bracerightmonospace;FF5D +bracerightsmall;FE5C +bracerighttp;F8FC +bracerightvertical;FE38 +bracketleft;005B +bracketleftbt;F8F0 +bracketleftex;F8EF +bracketleftmonospace;FF3B +bracketlefttp;F8EE +bracketright;005D +bracketrightbt;F8FB +bracketrightex;F8FA +bracketrightmonospace;FF3D +bracketrighttp;F8F9 +breve;02D8 +brevebelowcmb;032E +brevecmb;0306 +breveinvertedbelowcmb;032F +breveinvertedcmb;0311 +breveinverteddoublecmb;0361 +bridgebelowcmb;032A +bridgeinvertedbelowcmb;033A +brokenbar;00A6 +bstroke;0180 +bsuperior;F6EA +btopbar;0183 +buhiragana;3076 +bukatakana;30D6 +bullet;2022 +bulletinverse;25D8 +bulletoperator;2219 +bullseye;25CE +c;0063 +caarmenian;056E +cabengali;099A +cacute;0107 +cadeva;091A +cagujarati;0A9A +cagurmukhi;0A1A +calsquare;3388 +candrabindubengali;0981 +candrabinducmb;0310 +candrabindudeva;0901 +candrabindugujarati;0A81 +capslock;21EA +careof;2105 +caron;02C7 +caronbelowcmb;032C +caroncmb;030C +carriagereturn;21B5 +cbopomofo;3118 +ccaron;010D +ccedilla;00E7 +ccedillaacute;1E09 +ccircle;24D2 +ccircumflex;0109 +ccurl;0255 +cdot;010B +cdotaccent;010B +cdsquare;33C5 +cedilla;00B8 +cedillacmb;0327 +cent;00A2 +centigrade;2103 +centinferior;F6DF +centmonospace;FFE0 +centoldstyle;F7A2 +centsuperior;F6E0 +chaarmenian;0579 +chabengali;099B +chadeva;091B +chagujarati;0A9B +chagurmukhi;0A1B +chbopomofo;3114 +cheabkhasiancyrillic;04BD +checkmark;2713 +checyrillic;0447 +chedescenderabkhasiancyrillic;04BF +chedescendercyrillic;04B7 +chedieresiscyrillic;04F5 +cheharmenian;0573 +chekhakassiancyrillic;04CC +cheverticalstrokecyrillic;04B9 +chi;03C7 +chieuchacirclekorean;3277 +chieuchaparenkorean;3217 +chieuchcirclekorean;3269 +chieuchkorean;314A +chieuchparenkorean;3209 +chochangthai;0E0A +chochanthai;0E08 +chochingthai;0E09 +chochoethai;0E0C +chook;0188 +cieucacirclekorean;3276 +cieucaparenkorean;3216 +cieuccirclekorean;3268 +cieuckorean;3148 +cieucparenkorean;3208 +cieucuparenkorean;321C +circle;25CB +circlemultiply;2297 +circleot;2299 +circleplus;2295 +circlepostalmark;3036 +circlewithlefthalfblack;25D0 +circlewithrighthalfblack;25D1 +circumflex;02C6 +circumflexbelowcmb;032D +circumflexcmb;0302 +clear;2327 +clickalveolar;01C2 +clickdental;01C0 +clicklateral;01C1 +clickretroflex;01C3 +club;2663 +clubsuitblack;2663 +clubsuitwhite;2667 +cmcubedsquare;33A4 +cmonospace;FF43 +cmsquaredsquare;33A0 +coarmenian;0581 +colon;003A +colonmonetary;20A1 +colonmonospace;FF1A +colonsign;20A1 +colonsmall;FE55 +colontriangularhalfmod;02D1 +colontriangularmod;02D0 +comma;002C +commaabovecmb;0313 +commaaboverightcmb;0315 +commaaccent;F6C3 +commaarabic;060C +commaarmenian;055D +commainferior;F6E1 +commamonospace;FF0C +commareversedabovecmb;0314 +commareversedmod;02BD +commasmall;FE50 +commasuperior;F6E2 +commaturnedabovecmb;0312 +commaturnedmod;02BB +compass;263C +congruent;2245 +contourintegral;222E +control;2303 +controlACK;0006 +controlBEL;0007 +controlBS;0008 +controlCAN;0018 +controlCR;000D +controlDC1;0011 +controlDC2;0012 +controlDC3;0013 +controlDC4;0014 +controlDEL;007F +controlDLE;0010 +controlEM;0019 +controlENQ;0005 +controlEOT;0004 +controlESC;001B +controlETB;0017 +controlETX;0003 +controlFF;000C +controlFS;001C +controlGS;001D +controlHT;0009 +controlLF;000A +controlNAK;0015 +controlRS;001E +controlSI;000F +controlSO;000E +controlSOT;0002 +controlSTX;0001 +controlSUB;001A +controlSYN;0016 +controlUS;001F +controlVT;000B +copyright;00A9 +copyrightsans;F8E9 +copyrightserif;F6D9 +cornerbracketleft;300C +cornerbracketlefthalfwidth;FF62 +cornerbracketleftvertical;FE41 +cornerbracketright;300D +cornerbracketrighthalfwidth;FF63 +cornerbracketrightvertical;FE42 +corporationsquare;337F +cosquare;33C7 +coverkgsquare;33C6 +cparen;249E +cruzeiro;20A2 +cstretched;0297 +curlyand;22CF +curlyor;22CE +currency;00A4 +cyrBreve;F6D1 +cyrFlex;F6D2 +cyrbreve;F6D4 +cyrflex;F6D5 +d;0064 +daarmenian;0564 +dabengali;09A6 +dadarabic;0636 +dadeva;0926 +dadfinalarabic;FEBE +dadinitialarabic;FEBF +dadmedialarabic;FEC0 +dagesh;05BC +dageshhebrew;05BC +dagger;2020 +daggerdbl;2021 +dagujarati;0AA6 +dagurmukhi;0A26 +dahiragana;3060 +dakatakana;30C0 +dalarabic;062F +dalet;05D3 +daletdagesh;FB33 +daletdageshhebrew;FB33 +dalethatafpatah;05D3 05B2 +dalethatafpatahhebrew;05D3 05B2 +dalethatafsegol;05D3 05B1 +dalethatafsegolhebrew;05D3 05B1 +dalethebrew;05D3 +dalethiriq;05D3 05B4 +dalethiriqhebrew;05D3 05B4 +daletholam;05D3 05B9 +daletholamhebrew;05D3 05B9 +daletpatah;05D3 05B7 +daletpatahhebrew;05D3 05B7 +daletqamats;05D3 05B8 +daletqamatshebrew;05D3 05B8 +daletqubuts;05D3 05BB +daletqubutshebrew;05D3 05BB +daletsegol;05D3 05B6 +daletsegolhebrew;05D3 05B6 +daletsheva;05D3 05B0 +daletshevahebrew;05D3 05B0 +dalettsere;05D3 05B5 +dalettserehebrew;05D3 05B5 +dalfinalarabic;FEAA +dammaarabic;064F +dammalowarabic;064F +dammatanaltonearabic;064C +dammatanarabic;064C +danda;0964 +dargahebrew;05A7 +dargalefthebrew;05A7 +dasiapneumatacyrilliccmb;0485 +dblGrave;F6D3 +dblanglebracketleft;300A +dblanglebracketleftvertical;FE3D +dblanglebracketright;300B +dblanglebracketrightvertical;FE3E +dblarchinvertedbelowcmb;032B +dblarrowleft;21D4 +dblarrowright;21D2 +dbldanda;0965 +dblgrave;F6D6 +dblgravecmb;030F +dblintegral;222C +dbllowline;2017 +dbllowlinecmb;0333 +dbloverlinecmb;033F +dblprimemod;02BA +dblverticalbar;2016 +dblverticallineabovecmb;030E +dbopomofo;3109 +dbsquare;33C8 +dcaron;010F +dcedilla;1E11 +dcircle;24D3 +dcircumflexbelow;1E13 +dcroat;0111 +ddabengali;09A1 +ddadeva;0921 +ddagujarati;0AA1 +ddagurmukhi;0A21 +ddalarabic;0688 +ddalfinalarabic;FB89 +dddhadeva;095C +ddhabengali;09A2 +ddhadeva;0922 +ddhagujarati;0AA2 +ddhagurmukhi;0A22 +ddotaccent;1E0B +ddotbelow;1E0D +decimalseparatorarabic;066B +decimalseparatorpersian;066B +decyrillic;0434 +degree;00B0 +dehihebrew;05AD +dehiragana;3067 +deicoptic;03EF +dekatakana;30C7 +deleteleft;232B +deleteright;2326 +delta;03B4 +deltaturned;018D +denominatorminusonenumeratorbengali;09F8 +dezh;02A4 +dhabengali;09A7 +dhadeva;0927 +dhagujarati;0AA7 +dhagurmukhi;0A27 +dhook;0257 +dialytikatonos;0385 +dialytikatonoscmb;0344 +diamond;2666 +diamondsuitwhite;2662 +dieresis;00A8 +dieresisacute;F6D7 +dieresisbelowcmb;0324 +dieresiscmb;0308 +dieresisgrave;F6D8 +dieresistonos;0385 +dihiragana;3062 +dikatakana;30C2 +dittomark;3003 +divide;00F7 +divides;2223 +divisionslash;2215 +djecyrillic;0452 +dkshade;2593 +dlinebelow;1E0F +dlsquare;3397 +dmacron;0111 +dmonospace;FF44 +dnblock;2584 +dochadathai;0E0E +dodekthai;0E14 +dohiragana;3069 +dokatakana;30C9 +dollar;0024 +dollarinferior;F6E3 +dollarmonospace;FF04 +dollaroldstyle;F724 +dollarsmall;FE69 +dollarsuperior;F6E4 +dong;20AB +dorusquare;3326 +dotaccent;02D9 +dotaccentcmb;0307 +dotbelowcmb;0323 +dotbelowcomb;0323 +dotkatakana;30FB +dotlessi;0131 +dotlessj;F6BE +dotlessjstrokehook;0284 +dotmath;22C5 +dottedcircle;25CC +doubleyodpatah;FB1F +doubleyodpatahhebrew;FB1F +downtackbelowcmb;031E +downtackmod;02D5 +dparen;249F +dsuperior;F6EB +dtail;0256 +dtopbar;018C +duhiragana;3065 +dukatakana;30C5 +dz;01F3 +dzaltone;02A3 +dzcaron;01C6 +dzcurl;02A5 +dzeabkhasiancyrillic;04E1 +dzecyrillic;0455 +dzhecyrillic;045F +e;0065 +eacute;00E9 +earth;2641 +ebengali;098F +ebopomofo;311C +ebreve;0115 +ecandradeva;090D +ecandragujarati;0A8D +ecandravowelsigndeva;0945 +ecandravowelsigngujarati;0AC5 +ecaron;011B +ecedillabreve;1E1D +echarmenian;0565 +echyiwnarmenian;0587 +ecircle;24D4 +ecircumflex;00EA +ecircumflexacute;1EBF +ecircumflexbelow;1E19 +ecircumflexdotbelow;1EC7 +ecircumflexgrave;1EC1 +ecircumflexhookabove;1EC3 +ecircumflextilde;1EC5 +ecyrillic;0454 +edblgrave;0205 +edeva;090F +edieresis;00EB +edot;0117 +edotaccent;0117 +edotbelow;1EB9 +eegurmukhi;0A0F +eematragurmukhi;0A47 +efcyrillic;0444 +egrave;00E8 +egujarati;0A8F +eharmenian;0567 +ehbopomofo;311D +ehiragana;3048 +ehookabove;1EBB +eibopomofo;311F +eight;0038 +eightarabic;0668 +eightbengali;09EE +eightcircle;2467 +eightcircleinversesansserif;2791 +eightdeva;096E +eighteencircle;2471 +eighteenparen;2485 +eighteenperiod;2499 +eightgujarati;0AEE +eightgurmukhi;0A6E +eighthackarabic;0668 +eighthangzhou;3028 +eighthnotebeamed;266B +eightideographicparen;3227 +eightinferior;2088 +eightmonospace;FF18 +eightoldstyle;F738 +eightparen;247B +eightperiod;248F +eightpersian;06F8 +eightroman;2177 +eightsuperior;2078 +eightthai;0E58 +einvertedbreve;0207 +eiotifiedcyrillic;0465 +ekatakana;30A8 +ekatakanahalfwidth;FF74 +ekonkargurmukhi;0A74 +ekorean;3154 +elcyrillic;043B +element;2208 +elevencircle;246A +elevenparen;247E +elevenperiod;2492 +elevenroman;217A +ellipsis;2026 +ellipsisvertical;22EE +emacron;0113 +emacronacute;1E17 +emacrongrave;1E15 +emcyrillic;043C +emdash;2014 +emdashvertical;FE31 +emonospace;FF45 +emphasismarkarmenian;055B +emptyset;2205 +enbopomofo;3123 +encyrillic;043D +endash;2013 +endashvertical;FE32 +endescendercyrillic;04A3 +eng;014B +engbopomofo;3125 +enghecyrillic;04A5 +enhookcyrillic;04C8 +enspace;2002 +eogonek;0119 +eokorean;3153 +eopen;025B +eopenclosed;029A +eopenreversed;025C +eopenreversedclosed;025E +eopenreversedhook;025D +eparen;24A0 +epsilon;03B5 +epsilontonos;03AD +equal;003D +equalmonospace;FF1D +equalsmall;FE66 +equalsuperior;207C +equivalence;2261 +erbopomofo;3126 +ercyrillic;0440 +ereversed;0258 +ereversedcyrillic;044D +escyrillic;0441 +esdescendercyrillic;04AB +esh;0283 +eshcurl;0286 +eshortdeva;090E +eshortvowelsigndeva;0946 +eshreversedloop;01AA +eshsquatreversed;0285 +esmallhiragana;3047 +esmallkatakana;30A7 +esmallkatakanahalfwidth;FF6A +estimated;212E +esuperior;F6EC +eta;03B7 +etarmenian;0568 +etatonos;03AE +eth;00F0 +etilde;1EBD +etildebelow;1E1B +etnahtafoukhhebrew;0591 +etnahtafoukhlefthebrew;0591 +etnahtahebrew;0591 +etnahtalefthebrew;0591 +eturned;01DD +eukorean;3161 +euro;20AC +evowelsignbengali;09C7 +evowelsigndeva;0947 +evowelsigngujarati;0AC7 +exclam;0021 +exclamarmenian;055C +exclamdbl;203C +exclamdown;00A1 +exclamdownsmall;F7A1 +exclammonospace;FF01 +exclamsmall;F721 +existential;2203 +ezh;0292 +ezhcaron;01EF +ezhcurl;0293 +ezhreversed;01B9 +ezhtail;01BA +f;0066 +fadeva;095E +fagurmukhi;0A5E +fahrenheit;2109 +fathaarabic;064E +fathalowarabic;064E +fathatanarabic;064B +fbopomofo;3108 +fcircle;24D5 +fdotaccent;1E1F +feharabic;0641 +feharmenian;0586 +fehfinalarabic;FED2 +fehinitialarabic;FED3 +fehmedialarabic;FED4 +feicoptic;03E5 +female;2640 +ff;FB00 +ffi;FB03 +ffl;FB04 +fi;FB01 +fifteencircle;246E +fifteenparen;2482 +fifteenperiod;2496 +figuredash;2012 +filledbox;25A0 +filledrect;25AC +finalkaf;05DA +finalkafdagesh;FB3A +finalkafdageshhebrew;FB3A +finalkafhebrew;05DA +finalkafqamats;05DA 05B8 +finalkafqamatshebrew;05DA 05B8 +finalkafsheva;05DA 05B0 +finalkafshevahebrew;05DA 05B0 +finalmem;05DD +finalmemhebrew;05DD +finalnun;05DF +finalnunhebrew;05DF +finalpe;05E3 +finalpehebrew;05E3 +finaltsadi;05E5 +finaltsadihebrew;05E5 +firsttonechinese;02C9 +fisheye;25C9 +fitacyrillic;0473 +five;0035 +fivearabic;0665 +fivebengali;09EB +fivecircle;2464 +fivecircleinversesansserif;278E +fivedeva;096B +fiveeighths;215D +fivegujarati;0AEB +fivegurmukhi;0A6B +fivehackarabic;0665 +fivehangzhou;3025 +fiveideographicparen;3224 +fiveinferior;2085 +fivemonospace;FF15 +fiveoldstyle;F735 +fiveparen;2478 +fiveperiod;248C +fivepersian;06F5 +fiveroman;2174 +fivesuperior;2075 +fivethai;0E55 +fl;FB02 +florin;0192 +fmonospace;FF46 +fmsquare;3399 +fofanthai;0E1F +fofathai;0E1D +fongmanthai;0E4F +forall;2200 +four;0034 +fourarabic;0664 +fourbengali;09EA +fourcircle;2463 +fourcircleinversesansserif;278D +fourdeva;096A +fourgujarati;0AEA +fourgurmukhi;0A6A +fourhackarabic;0664 +fourhangzhou;3024 +fourideographicparen;3223 +fourinferior;2084 +fourmonospace;FF14 +fournumeratorbengali;09F7 +fouroldstyle;F734 +fourparen;2477 +fourperiod;248B +fourpersian;06F4 +fourroman;2173 +foursuperior;2074 +fourteencircle;246D +fourteenparen;2481 +fourteenperiod;2495 +fourthai;0E54 +fourthtonechinese;02CB +fparen;24A1 +fraction;2044 +franc;20A3 +g;0067 +gabengali;0997 +gacute;01F5 +gadeva;0917 +gafarabic;06AF +gaffinalarabic;FB93 +gafinitialarabic;FB94 +gafmedialarabic;FB95 +gagujarati;0A97 +gagurmukhi;0A17 +gahiragana;304C +gakatakana;30AC +gamma;03B3 +gammalatinsmall;0263 +gammasuperior;02E0 +gangiacoptic;03EB +gbopomofo;310D +gbreve;011F +gcaron;01E7 +gcedilla;0123 +gcircle;24D6 +gcircumflex;011D +gcommaaccent;0123 +gdot;0121 +gdotaccent;0121 +gecyrillic;0433 +gehiragana;3052 +gekatakana;30B2 +geometricallyequal;2251 +gereshaccenthebrew;059C +gereshhebrew;05F3 +gereshmuqdamhebrew;059D +germandbls;00DF +gershayimaccenthebrew;059E +gershayimhebrew;05F4 +getamark;3013 +ghabengali;0998 +ghadarmenian;0572 +ghadeva;0918 +ghagujarati;0A98 +ghagurmukhi;0A18 +ghainarabic;063A +ghainfinalarabic;FECE +ghaininitialarabic;FECF +ghainmedialarabic;FED0 +ghemiddlehookcyrillic;0495 +ghestrokecyrillic;0493 +gheupturncyrillic;0491 +ghhadeva;095A +ghhagurmukhi;0A5A +ghook;0260 +ghzsquare;3393 +gihiragana;304E +gikatakana;30AE +gimarmenian;0563 +gimel;05D2 +gimeldagesh;FB32 +gimeldageshhebrew;FB32 +gimelhebrew;05D2 +gjecyrillic;0453 +glottalinvertedstroke;01BE +glottalstop;0294 +glottalstopinverted;0296 +glottalstopmod;02C0 +glottalstopreversed;0295 +glottalstopreversedmod;02C1 +glottalstopreversedsuperior;02E4 +glottalstopstroke;02A1 +glottalstopstrokereversed;02A2 +gmacron;1E21 +gmonospace;FF47 +gohiragana;3054 +gokatakana;30B4 +gparen;24A2 +gpasquare;33AC +gradient;2207 +grave;0060 +gravebelowcmb;0316 +gravecmb;0300 +gravecomb;0300 +gravedeva;0953 +gravelowmod;02CE +gravemonospace;FF40 +gravetonecmb;0340 +greater;003E +greaterequal;2265 +greaterequalorless;22DB +greatermonospace;FF1E +greaterorequivalent;2273 +greaterorless;2277 +greateroverequal;2267 +greatersmall;FE65 +gscript;0261 +gstroke;01E5 +guhiragana;3050 +guillemotleft;00AB +guillemotright;00BB +guilsinglleft;2039 +guilsinglright;203A +gukatakana;30B0 +guramusquare;3318 +gysquare;33C9 +h;0068 +haabkhasiancyrillic;04A9 +haaltonearabic;06C1 +habengali;09B9 +hadescendercyrillic;04B3 +hadeva;0939 +hagujarati;0AB9 +hagurmukhi;0A39 +haharabic;062D +hahfinalarabic;FEA2 +hahinitialarabic;FEA3 +hahiragana;306F +hahmedialarabic;FEA4 +haitusquare;332A +hakatakana;30CF +hakatakanahalfwidth;FF8A +halantgurmukhi;0A4D +hamzaarabic;0621 +hamzadammaarabic;0621 064F +hamzadammatanarabic;0621 064C +hamzafathaarabic;0621 064E +hamzafathatanarabic;0621 064B +hamzalowarabic;0621 +hamzalowkasraarabic;0621 0650 +hamzalowkasratanarabic;0621 064D +hamzasukunarabic;0621 0652 +hangulfiller;3164 +hardsigncyrillic;044A +harpoonleftbarbup;21BC +harpoonrightbarbup;21C0 +hasquare;33CA +hatafpatah;05B2 +hatafpatah16;05B2 +hatafpatah23;05B2 +hatafpatah2f;05B2 +hatafpatahhebrew;05B2 +hatafpatahnarrowhebrew;05B2 +hatafpatahquarterhebrew;05B2 +hatafpatahwidehebrew;05B2 +hatafqamats;05B3 +hatafqamats1b;05B3 +hatafqamats28;05B3 +hatafqamats34;05B3 +hatafqamatshebrew;05B3 +hatafqamatsnarrowhebrew;05B3 +hatafqamatsquarterhebrew;05B3 +hatafqamatswidehebrew;05B3 +hatafsegol;05B1 +hatafsegol17;05B1 +hatafsegol24;05B1 +hatafsegol30;05B1 +hatafsegolhebrew;05B1 +hatafsegolnarrowhebrew;05B1 +hatafsegolquarterhebrew;05B1 +hatafsegolwidehebrew;05B1 +hbar;0127 +hbopomofo;310F +hbrevebelow;1E2B +hcedilla;1E29 +hcircle;24D7 +hcircumflex;0125 +hdieresis;1E27 +hdotaccent;1E23 +hdotbelow;1E25 +he;05D4 +heart;2665 +heartsuitblack;2665 +heartsuitwhite;2661 +hedagesh;FB34 +hedageshhebrew;FB34 +hehaltonearabic;06C1 +heharabic;0647 +hehebrew;05D4 +hehfinalaltonearabic;FBA7 +hehfinalalttwoarabic;FEEA +hehfinalarabic;FEEA +hehhamzaabovefinalarabic;FBA5 +hehhamzaaboveisolatedarabic;FBA4 +hehinitialaltonearabic;FBA8 +hehinitialarabic;FEEB +hehiragana;3078 +hehmedialaltonearabic;FBA9 +hehmedialarabic;FEEC +heiseierasquare;337B +hekatakana;30D8 +hekatakanahalfwidth;FF8D +hekutaarusquare;3336 +henghook;0267 +herutusquare;3339 +het;05D7 +hethebrew;05D7 +hhook;0266 +hhooksuperior;02B1 +hieuhacirclekorean;327B +hieuhaparenkorean;321B +hieuhcirclekorean;326D +hieuhkorean;314E +hieuhparenkorean;320D +hihiragana;3072 +hikatakana;30D2 +hikatakanahalfwidth;FF8B +hiriq;05B4 +hiriq14;05B4 +hiriq21;05B4 +hiriq2d;05B4 +hiriqhebrew;05B4 +hiriqnarrowhebrew;05B4 +hiriqquarterhebrew;05B4 +hiriqwidehebrew;05B4 +hlinebelow;1E96 +hmonospace;FF48 +hoarmenian;0570 +hohipthai;0E2B +hohiragana;307B +hokatakana;30DB +hokatakanahalfwidth;FF8E +holam;05B9 +holam19;05B9 +holam26;05B9 +holam32;05B9 +holamhebrew;05B9 +holamnarrowhebrew;05B9 +holamquarterhebrew;05B9 +holamwidehebrew;05B9 +honokhukthai;0E2E +hookabovecomb;0309 +hookcmb;0309 +hookpalatalizedbelowcmb;0321 +hookretroflexbelowcmb;0322 +hoonsquare;3342 +horicoptic;03E9 +horizontalbar;2015 +horncmb;031B +hotsprings;2668 +house;2302 +hparen;24A3 +hsuperior;02B0 +hturned;0265 +huhiragana;3075 +huiitosquare;3333 +hukatakana;30D5 +hukatakanahalfwidth;FF8C +hungarumlaut;02DD +hungarumlautcmb;030B +hv;0195 +hyphen;002D +hypheninferior;F6E5 +hyphenmonospace;FF0D +hyphensmall;FE63 +hyphensuperior;F6E6 +hyphentwo;2010 +i;0069 +iacute;00ED +iacyrillic;044F +ibengali;0987 +ibopomofo;3127 +ibreve;012D +icaron;01D0 +icircle;24D8 +icircumflex;00EE +icyrillic;0456 +idblgrave;0209 +ideographearthcircle;328F +ideographfirecircle;328B +ideographicallianceparen;323F +ideographiccallparen;323A +ideographiccentrecircle;32A5 +ideographicclose;3006 +ideographiccomma;3001 +ideographiccommaleft;FF64 +ideographiccongratulationparen;3237 +ideographiccorrectcircle;32A3 +ideographicearthparen;322F +ideographicenterpriseparen;323D +ideographicexcellentcircle;329D +ideographicfestivalparen;3240 +ideographicfinancialcircle;3296 +ideographicfinancialparen;3236 +ideographicfireparen;322B +ideographichaveparen;3232 +ideographichighcircle;32A4 +ideographiciterationmark;3005 +ideographiclaborcircle;3298 +ideographiclaborparen;3238 +ideographicleftcircle;32A7 +ideographiclowcircle;32A6 +ideographicmedicinecircle;32A9 +ideographicmetalparen;322E +ideographicmoonparen;322A +ideographicnameparen;3234 +ideographicperiod;3002 +ideographicprintcircle;329E +ideographicreachparen;3243 +ideographicrepresentparen;3239 +ideographicresourceparen;323E +ideographicrightcircle;32A8 +ideographicsecretcircle;3299 +ideographicselfparen;3242 +ideographicsocietyparen;3233 +ideographicspace;3000 +ideographicspecialparen;3235 +ideographicstockparen;3231 +ideographicstudyparen;323B +ideographicsunparen;3230 +ideographicsuperviseparen;323C +ideographicwaterparen;322C +ideographicwoodparen;322D +ideographiczero;3007 +ideographmetalcircle;328E +ideographmooncircle;328A +ideographnamecircle;3294 +ideographsuncircle;3290 +ideographwatercircle;328C +ideographwoodcircle;328D +ideva;0907 +idieresis;00EF +idieresisacute;1E2F +idieresiscyrillic;04E5 +idotbelow;1ECB +iebrevecyrillic;04D7 +iecyrillic;0435 +ieungacirclekorean;3275 +ieungaparenkorean;3215 +ieungcirclekorean;3267 +ieungkorean;3147 +ieungparenkorean;3207 +igrave;00EC +igujarati;0A87 +igurmukhi;0A07 +ihiragana;3044 +ihookabove;1EC9 +iibengali;0988 +iicyrillic;0438 +iideva;0908 +iigujarati;0A88 +iigurmukhi;0A08 +iimatragurmukhi;0A40 +iinvertedbreve;020B +iishortcyrillic;0439 +iivowelsignbengali;09C0 +iivowelsigndeva;0940 +iivowelsigngujarati;0AC0 +ij;0133 +ikatakana;30A4 +ikatakanahalfwidth;FF72 +ikorean;3163 +ilde;02DC +iluyhebrew;05AC +imacron;012B +imacroncyrillic;04E3 +imageorapproximatelyequal;2253 +imatragurmukhi;0A3F +imonospace;FF49 +increment;2206 +infinity;221E +iniarmenian;056B +integral;222B +integralbottom;2321 +integralbt;2321 +integralex;F8F5 +integraltop;2320 +integraltp;2320 +intersection;2229 +intisquare;3305 +invbullet;25D8 +invcircle;25D9 +invsmileface;263B +iocyrillic;0451 +iogonek;012F +iota;03B9 +iotadieresis;03CA +iotadieresistonos;0390 +iotalatin;0269 +iotatonos;03AF +iparen;24A4 +irigurmukhi;0A72 +ismallhiragana;3043 +ismallkatakana;30A3 +ismallkatakanahalfwidth;FF68 +issharbengali;09FA +istroke;0268 +isuperior;F6ED +iterationhiragana;309D +iterationkatakana;30FD +itilde;0129 +itildebelow;1E2D +iubopomofo;3129 +iucyrillic;044E +ivowelsignbengali;09BF +ivowelsigndeva;093F +ivowelsigngujarati;0ABF +izhitsacyrillic;0475 +izhitsadblgravecyrillic;0477 +j;006A +jaarmenian;0571 +jabengali;099C +jadeva;091C +jagujarati;0A9C +jagurmukhi;0A1C +jbopomofo;3110 +jcaron;01F0 +jcircle;24D9 +jcircumflex;0135 +jcrossedtail;029D +jdotlessstroke;025F +jecyrillic;0458 +jeemarabic;062C +jeemfinalarabic;FE9E +jeeminitialarabic;FE9F +jeemmedialarabic;FEA0 +jeharabic;0698 +jehfinalarabic;FB8B +jhabengali;099D +jhadeva;091D +jhagujarati;0A9D +jhagurmukhi;0A1D +jheharmenian;057B +jis;3004 +jmonospace;FF4A +jparen;24A5 +jsuperior;02B2 +k;006B +kabashkircyrillic;04A1 +kabengali;0995 +kacute;1E31 +kacyrillic;043A +kadescendercyrillic;049B +kadeva;0915 +kaf;05DB +kafarabic;0643 +kafdagesh;FB3B +kafdageshhebrew;FB3B +kaffinalarabic;FEDA +kafhebrew;05DB +kafinitialarabic;FEDB +kafmedialarabic;FEDC +kafrafehebrew;FB4D +kagujarati;0A95 +kagurmukhi;0A15 +kahiragana;304B +kahookcyrillic;04C4 +kakatakana;30AB +kakatakanahalfwidth;FF76 +kappa;03BA +kappasymbolgreek;03F0 +kapyeounmieumkorean;3171 +kapyeounphieuphkorean;3184 +kapyeounpieupkorean;3178 +kapyeounssangpieupkorean;3179 +karoriisquare;330D +kashidaautoarabic;0640 +kashidaautonosidebearingarabic;0640 +kasmallkatakana;30F5 +kasquare;3384 +kasraarabic;0650 +kasratanarabic;064D +kastrokecyrillic;049F +katahiraprolongmarkhalfwidth;FF70 +kaverticalstrokecyrillic;049D +kbopomofo;310E +kcalsquare;3389 +kcaron;01E9 +kcedilla;0137 +kcircle;24DA +kcommaaccent;0137 +kdotbelow;1E33 +keharmenian;0584 +kehiragana;3051 +kekatakana;30B1 +kekatakanahalfwidth;FF79 +kenarmenian;056F +kesmallkatakana;30F6 +kgreenlandic;0138 +khabengali;0996 +khacyrillic;0445 +khadeva;0916 +khagujarati;0A96 +khagurmukhi;0A16 +khaharabic;062E +khahfinalarabic;FEA6 +khahinitialarabic;FEA7 +khahmedialarabic;FEA8 +kheicoptic;03E7 +khhadeva;0959 +khhagurmukhi;0A59 +khieukhacirclekorean;3278 +khieukhaparenkorean;3218 +khieukhcirclekorean;326A +khieukhkorean;314B +khieukhparenkorean;320A +khokhaithai;0E02 +khokhonthai;0E05 +khokhuatthai;0E03 +khokhwaithai;0E04 +khomutthai;0E5B +khook;0199 +khorakhangthai;0E06 +khzsquare;3391 +kihiragana;304D +kikatakana;30AD +kikatakanahalfwidth;FF77 +kiroguramusquare;3315 +kiromeetorusquare;3316 +kirosquare;3314 +kiyeokacirclekorean;326E +kiyeokaparenkorean;320E +kiyeokcirclekorean;3260 +kiyeokkorean;3131 +kiyeokparenkorean;3200 +kiyeoksioskorean;3133 +kjecyrillic;045C +klinebelow;1E35 +klsquare;3398 +kmcubedsquare;33A6 +kmonospace;FF4B +kmsquaredsquare;33A2 +kohiragana;3053 +kohmsquare;33C0 +kokaithai;0E01 +kokatakana;30B3 +kokatakanahalfwidth;FF7A +kooposquare;331E +koppacyrillic;0481 +koreanstandardsymbol;327F +koroniscmb;0343 +kparen;24A6 +kpasquare;33AA +ksicyrillic;046F +ktsquare;33CF +kturned;029E +kuhiragana;304F +kukatakana;30AF +kukatakanahalfwidth;FF78 +kvsquare;33B8 +kwsquare;33BE +l;006C +labengali;09B2 +lacute;013A +ladeva;0932 +lagujarati;0AB2 +lagurmukhi;0A32 +lakkhangyaothai;0E45 +lamaleffinalarabic;FEFC +lamalefhamzaabovefinalarabic;FEF8 +lamalefhamzaaboveisolatedarabic;FEF7 +lamalefhamzabelowfinalarabic;FEFA +lamalefhamzabelowisolatedarabic;FEF9 +lamalefisolatedarabic;FEFB +lamalefmaddaabovefinalarabic;FEF6 +lamalefmaddaaboveisolatedarabic;FEF5 +lamarabic;0644 +lambda;03BB +lambdastroke;019B +lamed;05DC +lameddagesh;FB3C +lameddageshhebrew;FB3C +lamedhebrew;05DC +lamedholam;05DC 05B9 +lamedholamdagesh;05DC 05B9 05BC +lamedholamdageshhebrew;05DC 05B9 05BC +lamedholamhebrew;05DC 05B9 +lamfinalarabic;FEDE +lamhahinitialarabic;FCCA +laminitialarabic;FEDF +lamjeeminitialarabic;FCC9 +lamkhahinitialarabic;FCCB +lamlamhehisolatedarabic;FDF2 +lammedialarabic;FEE0 +lammeemhahinitialarabic;FD88 +lammeeminitialarabic;FCCC +lammeemjeeminitialarabic;FEDF FEE4 FEA0 +lammeemkhahinitialarabic;FEDF FEE4 FEA8 +largecircle;25EF +lbar;019A +lbelt;026C +lbopomofo;310C +lcaron;013E +lcedilla;013C +lcircle;24DB +lcircumflexbelow;1E3D +lcommaaccent;013C +ldot;0140 +ldotaccent;0140 +ldotbelow;1E37 +ldotbelowmacron;1E39 +leftangleabovecmb;031A +lefttackbelowcmb;0318 +less;003C +lessequal;2264 +lessequalorgreater;22DA +lessmonospace;FF1C +lessorequivalent;2272 +lessorgreater;2276 +lessoverequal;2266 +lesssmall;FE64 +lezh;026E +lfblock;258C +lhookretroflex;026D +lira;20A4 +liwnarmenian;056C +lj;01C9 +ljecyrillic;0459 +ll;F6C0 +lladeva;0933 +llagujarati;0AB3 +llinebelow;1E3B +llladeva;0934 +llvocalicbengali;09E1 +llvocalicdeva;0961 +llvocalicvowelsignbengali;09E3 +llvocalicvowelsigndeva;0963 +lmiddletilde;026B +lmonospace;FF4C +lmsquare;33D0 +lochulathai;0E2C +logicaland;2227 +logicalnot;00AC +logicalnotreversed;2310 +logicalor;2228 +lolingthai;0E25 +longs;017F +lowlinecenterline;FE4E +lowlinecmb;0332 +lowlinedashed;FE4D +lozenge;25CA +lparen;24A7 +lslash;0142 +lsquare;2113 +lsuperior;F6EE +ltshade;2591 +luthai;0E26 +lvocalicbengali;098C +lvocalicdeva;090C +lvocalicvowelsignbengali;09E2 +lvocalicvowelsigndeva;0962 +lxsquare;33D3 +m;006D +mabengali;09AE +macron;00AF +macronbelowcmb;0331 +macroncmb;0304 +macronlowmod;02CD +macronmonospace;FFE3 +macute;1E3F +madeva;092E +magujarati;0AAE +magurmukhi;0A2E +mahapakhhebrew;05A4 +mahapakhlefthebrew;05A4 +mahiragana;307E +maichattawalowleftthai;F895 +maichattawalowrightthai;F894 +maichattawathai;0E4B +maichattawaupperleftthai;F893 +maieklowleftthai;F88C +maieklowrightthai;F88B +maiekthai;0E48 +maiekupperleftthai;F88A +maihanakatleftthai;F884 +maihanakatthai;0E31 +maitaikhuleftthai;F889 +maitaikhuthai;0E47 +maitholowleftthai;F88F +maitholowrightthai;F88E +maithothai;0E49 +maithoupperleftthai;F88D +maitrilowleftthai;F892 +maitrilowrightthai;F891 +maitrithai;0E4A +maitriupperleftthai;F890 +maiyamokthai;0E46 +makatakana;30DE +makatakanahalfwidth;FF8F +male;2642 +mansyonsquare;3347 +maqafhebrew;05BE +mars;2642 +masoracirclehebrew;05AF +masquare;3383 +mbopomofo;3107 +mbsquare;33D4 +mcircle;24DC +mcubedsquare;33A5 +mdotaccent;1E41 +mdotbelow;1E43 +meemarabic;0645 +meemfinalarabic;FEE2 +meeminitialarabic;FEE3 +meemmedialarabic;FEE4 +meemmeeminitialarabic;FCD1 +meemmeemisolatedarabic;FC48 +meetorusquare;334D +mehiragana;3081 +meizierasquare;337E +mekatakana;30E1 +mekatakanahalfwidth;FF92 +mem;05DE +memdagesh;FB3E +memdageshhebrew;FB3E +memhebrew;05DE +menarmenian;0574 +merkhahebrew;05A5 +merkhakefulahebrew;05A6 +merkhakefulalefthebrew;05A6 +merkhalefthebrew;05A5 +mhook;0271 +mhzsquare;3392 +middledotkatakanahalfwidth;FF65 +middot;00B7 +mieumacirclekorean;3272 +mieumaparenkorean;3212 +mieumcirclekorean;3264 +mieumkorean;3141 +mieumpansioskorean;3170 +mieumparenkorean;3204 +mieumpieupkorean;316E +mieumsioskorean;316F +mihiragana;307F +mikatakana;30DF +mikatakanahalfwidth;FF90 +minus;2212 +minusbelowcmb;0320 +minuscircle;2296 +minusmod;02D7 +minusplus;2213 +minute;2032 +miribaarusquare;334A +mirisquare;3349 +mlonglegturned;0270 +mlsquare;3396 +mmcubedsquare;33A3 +mmonospace;FF4D +mmsquaredsquare;339F +mohiragana;3082 +mohmsquare;33C1 +mokatakana;30E2 +mokatakanahalfwidth;FF93 +molsquare;33D6 +momathai;0E21 +moverssquare;33A7 +moverssquaredsquare;33A8 +mparen;24A8 +mpasquare;33AB +mssquare;33B3 +msuperior;F6EF +mturned;026F +mu;00B5 +mu1;00B5 +muasquare;3382 +muchgreater;226B +muchless;226A +mufsquare;338C +mugreek;03BC +mugsquare;338D +muhiragana;3080 +mukatakana;30E0 +mukatakanahalfwidth;FF91 +mulsquare;3395 +multiply;00D7 +mumsquare;339B +munahhebrew;05A3 +munahlefthebrew;05A3 +musicalnote;266A +musicalnotedbl;266B +musicflatsign;266D +musicsharpsign;266F +mussquare;33B2 +muvsquare;33B6 +muwsquare;33BC +mvmegasquare;33B9 +mvsquare;33B7 +mwmegasquare;33BF +mwsquare;33BD +n;006E +nabengali;09A8 +nabla;2207 +nacute;0144 +nadeva;0928 +nagujarati;0AA8 +nagurmukhi;0A28 +nahiragana;306A +nakatakana;30CA +nakatakanahalfwidth;FF85 +napostrophe;0149 +nasquare;3381 +nbopomofo;310B +nbspace;00A0 +ncaron;0148 +ncedilla;0146 +ncircle;24DD +ncircumflexbelow;1E4B +ncommaaccent;0146 +ndotaccent;1E45 +ndotbelow;1E47 +nehiragana;306D +nekatakana;30CD +nekatakanahalfwidth;FF88 +newsheqelsign;20AA +nfsquare;338B +ngabengali;0999 +ngadeva;0919 +ngagujarati;0A99 +ngagurmukhi;0A19 +ngonguthai;0E07 +nhiragana;3093 +nhookleft;0272 +nhookretroflex;0273 +nieunacirclekorean;326F +nieunaparenkorean;320F +nieuncieuckorean;3135 +nieuncirclekorean;3261 +nieunhieuhkorean;3136 +nieunkorean;3134 +nieunpansioskorean;3168 +nieunparenkorean;3201 +nieunsioskorean;3167 +nieuntikeutkorean;3166 +nihiragana;306B +nikatakana;30CB +nikatakanahalfwidth;FF86 +nikhahitleftthai;F899 +nikhahitthai;0E4D +nine;0039 +ninearabic;0669 +ninebengali;09EF +ninecircle;2468 +ninecircleinversesansserif;2792 +ninedeva;096F +ninegujarati;0AEF +ninegurmukhi;0A6F +ninehackarabic;0669 +ninehangzhou;3029 +nineideographicparen;3228 +nineinferior;2089 +ninemonospace;FF19 +nineoldstyle;F739 +nineparen;247C +nineperiod;2490 +ninepersian;06F9 +nineroman;2178 +ninesuperior;2079 +nineteencircle;2472 +nineteenparen;2486 +nineteenperiod;249A +ninethai;0E59 +nj;01CC +njecyrillic;045A +nkatakana;30F3 +nkatakanahalfwidth;FF9D +nlegrightlong;019E +nlinebelow;1E49 +nmonospace;FF4E +nmsquare;339A +nnabengali;09A3 +nnadeva;0923 +nnagujarati;0AA3 +nnagurmukhi;0A23 +nnnadeva;0929 +nohiragana;306E +nokatakana;30CE +nokatakanahalfwidth;FF89 +nonbreakingspace;00A0 +nonenthai;0E13 +nonuthai;0E19 +noonarabic;0646 +noonfinalarabic;FEE6 +noonghunnaarabic;06BA +noonghunnafinalarabic;FB9F +noonhehinitialarabic;FEE7 FEEC +nooninitialarabic;FEE7 +noonjeeminitialarabic;FCD2 +noonjeemisolatedarabic;FC4B +noonmedialarabic;FEE8 +noonmeeminitialarabic;FCD5 +noonmeemisolatedarabic;FC4E +noonnoonfinalarabic;FC8D +notcontains;220C +notelement;2209 +notelementof;2209 +notequal;2260 +notgreater;226F +notgreaternorequal;2271 +notgreaternorless;2279 +notidentical;2262 +notless;226E +notlessnorequal;2270 +notparallel;2226 +notprecedes;2280 +notsubset;2284 +notsucceeds;2281 +notsuperset;2285 +nowarmenian;0576 +nparen;24A9 +nssquare;33B1 +nsuperior;207F +ntilde;00F1 +nu;03BD +nuhiragana;306C +nukatakana;30CC +nukatakanahalfwidth;FF87 +nuktabengali;09BC +nuktadeva;093C +nuktagujarati;0ABC +nuktagurmukhi;0A3C +numbersign;0023 +numbersignmonospace;FF03 +numbersignsmall;FE5F +numeralsigngreek;0374 +numeralsignlowergreek;0375 +numero;2116 +nun;05E0 +nundagesh;FB40 +nundageshhebrew;FB40 +nunhebrew;05E0 +nvsquare;33B5 +nwsquare;33BB +nyabengali;099E +nyadeva;091E +nyagujarati;0A9E +nyagurmukhi;0A1E +o;006F +oacute;00F3 +oangthai;0E2D +obarred;0275 +obarredcyrillic;04E9 +obarreddieresiscyrillic;04EB +obengali;0993 +obopomofo;311B +obreve;014F +ocandradeva;0911 +ocandragujarati;0A91 +ocandravowelsigndeva;0949 +ocandravowelsigngujarati;0AC9 +ocaron;01D2 +ocircle;24DE +ocircumflex;00F4 +ocircumflexacute;1ED1 +ocircumflexdotbelow;1ED9 +ocircumflexgrave;1ED3 +ocircumflexhookabove;1ED5 +ocircumflextilde;1ED7 +ocyrillic;043E +odblacute;0151 +odblgrave;020D +odeva;0913 +odieresis;00F6 +odieresiscyrillic;04E7 +odotbelow;1ECD +oe;0153 +oekorean;315A +ogonek;02DB +ogonekcmb;0328 +ograve;00F2 +ogujarati;0A93 +oharmenian;0585 +ohiragana;304A +ohookabove;1ECF +ohorn;01A1 +ohornacute;1EDB +ohorndotbelow;1EE3 +ohorngrave;1EDD +ohornhookabove;1EDF +ohorntilde;1EE1 +ohungarumlaut;0151 +oi;01A3 +oinvertedbreve;020F +okatakana;30AA +okatakanahalfwidth;FF75 +okorean;3157 +olehebrew;05AB +omacron;014D +omacronacute;1E53 +omacrongrave;1E51 +omdeva;0950 +omega;03C9 +omega1;03D6 +omegacyrillic;0461 +omegalatinclosed;0277 +omegaroundcyrillic;047B +omegatitlocyrillic;047D +omegatonos;03CE +omgujarati;0AD0 +omicron;03BF +omicrontonos;03CC +omonospace;FF4F +one;0031 +onearabic;0661 +onebengali;09E7 +onecircle;2460 +onecircleinversesansserif;278A +onedeva;0967 +onedotenleader;2024 +oneeighth;215B +onefitted;F6DC +onegujarati;0AE7 +onegurmukhi;0A67 +onehackarabic;0661 +onehalf;00BD +onehangzhou;3021 +oneideographicparen;3220 +oneinferior;2081 +onemonospace;FF11 +onenumeratorbengali;09F4 +oneoldstyle;F731 +oneparen;2474 +oneperiod;2488 +onepersian;06F1 +onequarter;00BC +oneroman;2170 +onesuperior;00B9 +onethai;0E51 +onethird;2153 +oogonek;01EB +oogonekmacron;01ED +oogurmukhi;0A13 +oomatragurmukhi;0A4B +oopen;0254 +oparen;24AA +openbullet;25E6 +option;2325 +ordfeminine;00AA +ordmasculine;00BA +orthogonal;221F +oshortdeva;0912 +oshortvowelsigndeva;094A +oslash;00F8 +oslashacute;01FF +osmallhiragana;3049 +osmallkatakana;30A9 +osmallkatakanahalfwidth;FF6B +ostrokeacute;01FF +osuperior;F6F0 +otcyrillic;047F +otilde;00F5 +otildeacute;1E4D +otildedieresis;1E4F +oubopomofo;3121 +overline;203E +overlinecenterline;FE4A +overlinecmb;0305 +overlinedashed;FE49 +overlinedblwavy;FE4C +overlinewavy;FE4B +overscore;00AF +ovowelsignbengali;09CB +ovowelsigndeva;094B +ovowelsigngujarati;0ACB +p;0070 +paampssquare;3380 +paasentosquare;332B +pabengali;09AA +pacute;1E55 +padeva;092A +pagedown;21DF +pageup;21DE +pagujarati;0AAA +pagurmukhi;0A2A +pahiragana;3071 +paiyannoithai;0E2F +pakatakana;30D1 +palatalizationcyrilliccmb;0484 +palochkacyrillic;04C0 +pansioskorean;317F +paragraph;00B6 +parallel;2225 +parenleft;0028 +parenleftaltonearabic;FD3E +parenleftbt;F8ED +parenleftex;F8EC +parenleftinferior;208D +parenleftmonospace;FF08 +parenleftsmall;FE59 +parenleftsuperior;207D +parenlefttp;F8EB +parenleftvertical;FE35 +parenright;0029 +parenrightaltonearabic;FD3F +parenrightbt;F8F8 +parenrightex;F8F7 +parenrightinferior;208E +parenrightmonospace;FF09 +parenrightsmall;FE5A +parenrightsuperior;207E +parenrighttp;F8F6 +parenrightvertical;FE36 +partialdiff;2202 +paseqhebrew;05C0 +pashtahebrew;0599 +pasquare;33A9 +patah;05B7 +patah11;05B7 +patah1d;05B7 +patah2a;05B7 +patahhebrew;05B7 +patahnarrowhebrew;05B7 +patahquarterhebrew;05B7 +patahwidehebrew;05B7 +pazerhebrew;05A1 +pbopomofo;3106 +pcircle;24DF +pdotaccent;1E57 +pe;05E4 +pecyrillic;043F +pedagesh;FB44 +pedageshhebrew;FB44 +peezisquare;333B +pefinaldageshhebrew;FB43 +peharabic;067E +peharmenian;057A +pehebrew;05E4 +pehfinalarabic;FB57 +pehinitialarabic;FB58 +pehiragana;307A +pehmedialarabic;FB59 +pekatakana;30DA +pemiddlehookcyrillic;04A7 +perafehebrew;FB4E +percent;0025 +percentarabic;066A +percentmonospace;FF05 +percentsmall;FE6A +period;002E +periodarmenian;0589 +periodcentered;00B7 +periodhalfwidth;FF61 +periodinferior;F6E7 +periodmonospace;FF0E +periodsmall;FE52 +periodsuperior;F6E8 +perispomenigreekcmb;0342 +perpendicular;22A5 +perthousand;2030 +peseta;20A7 +pfsquare;338A +phabengali;09AB +phadeva;092B +phagujarati;0AAB +phagurmukhi;0A2B +phi;03C6 +phi1;03D5 +phieuphacirclekorean;327A +phieuphaparenkorean;321A +phieuphcirclekorean;326C +phieuphkorean;314D +phieuphparenkorean;320C +philatin;0278 +phinthuthai;0E3A +phisymbolgreek;03D5 +phook;01A5 +phophanthai;0E1E +phophungthai;0E1C +phosamphaothai;0E20 +pi;03C0 +pieupacirclekorean;3273 +pieupaparenkorean;3213 +pieupcieuckorean;3176 +pieupcirclekorean;3265 +pieupkiyeokkorean;3172 +pieupkorean;3142 +pieupparenkorean;3205 +pieupsioskiyeokkorean;3174 +pieupsioskorean;3144 +pieupsiostikeutkorean;3175 +pieupthieuthkorean;3177 +pieuptikeutkorean;3173 +pihiragana;3074 +pikatakana;30D4 +pisymbolgreek;03D6 +piwrarmenian;0583 +plus;002B +plusbelowcmb;031F +pluscircle;2295 +plusminus;00B1 +plusmod;02D6 +plusmonospace;FF0B +plussmall;FE62 +plussuperior;207A +pmonospace;FF50 +pmsquare;33D8 +pohiragana;307D +pointingindexdownwhite;261F +pointingindexleftwhite;261C +pointingindexrightwhite;261E +pointingindexupwhite;261D +pokatakana;30DD +poplathai;0E1B +postalmark;3012 +postalmarkface;3020 +pparen;24AB +precedes;227A +prescription;211E +primemod;02B9 +primereversed;2035 +product;220F +projective;2305 +prolongedkana;30FC +propellor;2318 +propersubset;2282 +propersuperset;2283 +proportion;2237 +proportional;221D +psi;03C8 +psicyrillic;0471 +psilipneumatacyrilliccmb;0486 +pssquare;33B0 +puhiragana;3077 +pukatakana;30D7 +pvsquare;33B4 +pwsquare;33BA +q;0071 +qadeva;0958 +qadmahebrew;05A8 +qafarabic;0642 +qaffinalarabic;FED6 +qafinitialarabic;FED7 +qafmedialarabic;FED8 +qamats;05B8 +qamats10;05B8 +qamats1a;05B8 +qamats1c;05B8 +qamats27;05B8 +qamats29;05B8 +qamats33;05B8 +qamatsde;05B8 +qamatshebrew;05B8 +qamatsnarrowhebrew;05B8 +qamatsqatanhebrew;05B8 +qamatsqatannarrowhebrew;05B8 +qamatsqatanquarterhebrew;05B8 +qamatsqatanwidehebrew;05B8 +qamatsquarterhebrew;05B8 +qamatswidehebrew;05B8 +qarneyparahebrew;059F +qbopomofo;3111 +qcircle;24E0 +qhook;02A0 +qmonospace;FF51 +qof;05E7 +qofdagesh;FB47 +qofdageshhebrew;FB47 +qofhatafpatah;05E7 05B2 +qofhatafpatahhebrew;05E7 05B2 +qofhatafsegol;05E7 05B1 +qofhatafsegolhebrew;05E7 05B1 +qofhebrew;05E7 +qofhiriq;05E7 05B4 +qofhiriqhebrew;05E7 05B4 +qofholam;05E7 05B9 +qofholamhebrew;05E7 05B9 +qofpatah;05E7 05B7 +qofpatahhebrew;05E7 05B7 +qofqamats;05E7 05B8 +qofqamatshebrew;05E7 05B8 +qofqubuts;05E7 05BB +qofqubutshebrew;05E7 05BB +qofsegol;05E7 05B6 +qofsegolhebrew;05E7 05B6 +qofsheva;05E7 05B0 +qofshevahebrew;05E7 05B0 +qoftsere;05E7 05B5 +qoftserehebrew;05E7 05B5 +qparen;24AC +quarternote;2669 +qubuts;05BB +qubuts18;05BB +qubuts25;05BB +qubuts31;05BB +qubutshebrew;05BB +qubutsnarrowhebrew;05BB +qubutsquarterhebrew;05BB +qubutswidehebrew;05BB +question;003F +questionarabic;061F +questionarmenian;055E +questiondown;00BF +questiondownsmall;F7BF +questiongreek;037E +questionmonospace;FF1F +questionsmall;F73F +quotedbl;0022 +quotedblbase;201E +quotedblleft;201C +quotedblmonospace;FF02 +quotedblprime;301E +quotedblprimereversed;301D +quotedblright;201D +quoteleft;2018 +quoteleftreversed;201B +quotereversed;201B +quoteright;2019 +quoterightn;0149 +quotesinglbase;201A +quotesingle;0027 +quotesinglemonospace;FF07 +r;0072 +raarmenian;057C +rabengali;09B0 +racute;0155 +radeva;0930 +radical;221A +radicalex;F8E5 +radoverssquare;33AE +radoverssquaredsquare;33AF +radsquare;33AD +rafe;05BF +rafehebrew;05BF +ragujarati;0AB0 +ragurmukhi;0A30 +rahiragana;3089 +rakatakana;30E9 +rakatakanahalfwidth;FF97 +ralowerdiagonalbengali;09F1 +ramiddlediagonalbengali;09F0 +ramshorn;0264 +ratio;2236 +rbopomofo;3116 +rcaron;0159 +rcedilla;0157 +rcircle;24E1 +rcommaaccent;0157 +rdblgrave;0211 +rdotaccent;1E59 +rdotbelow;1E5B +rdotbelowmacron;1E5D +referencemark;203B +reflexsubset;2286 +reflexsuperset;2287 +registered;00AE +registersans;F8E8 +registerserif;F6DA +reharabic;0631 +reharmenian;0580 +rehfinalarabic;FEAE +rehiragana;308C +rehyehaleflamarabic;0631 FEF3 FE8E 0644 +rekatakana;30EC +rekatakanahalfwidth;FF9A +resh;05E8 +reshdageshhebrew;FB48 +reshhatafpatah;05E8 05B2 +reshhatafpatahhebrew;05E8 05B2 +reshhatafsegol;05E8 05B1 +reshhatafsegolhebrew;05E8 05B1 +reshhebrew;05E8 +reshhiriq;05E8 05B4 +reshhiriqhebrew;05E8 05B4 +reshholam;05E8 05B9 +reshholamhebrew;05E8 05B9 +reshpatah;05E8 05B7 +reshpatahhebrew;05E8 05B7 +reshqamats;05E8 05B8 +reshqamatshebrew;05E8 05B8 +reshqubuts;05E8 05BB +reshqubutshebrew;05E8 05BB +reshsegol;05E8 05B6 +reshsegolhebrew;05E8 05B6 +reshsheva;05E8 05B0 +reshshevahebrew;05E8 05B0 +reshtsere;05E8 05B5 +reshtserehebrew;05E8 05B5 +reversedtilde;223D +reviahebrew;0597 +reviamugrashhebrew;0597 +revlogicalnot;2310 +rfishhook;027E +rfishhookreversed;027F +rhabengali;09DD +rhadeva;095D +rho;03C1 +rhook;027D +rhookturned;027B +rhookturnedsuperior;02B5 +rhosymbolgreek;03F1 +rhotichookmod;02DE +rieulacirclekorean;3271 +rieulaparenkorean;3211 +rieulcirclekorean;3263 +rieulhieuhkorean;3140 +rieulkiyeokkorean;313A +rieulkiyeoksioskorean;3169 +rieulkorean;3139 +rieulmieumkorean;313B +rieulpansioskorean;316C +rieulparenkorean;3203 +rieulphieuphkorean;313F +rieulpieupkorean;313C +rieulpieupsioskorean;316B +rieulsioskorean;313D +rieulthieuthkorean;313E +rieultikeutkorean;316A +rieulyeorinhieuhkorean;316D +rightangle;221F +righttackbelowcmb;0319 +righttriangle;22BF +rihiragana;308A +rikatakana;30EA +rikatakanahalfwidth;FF98 +ring;02DA +ringbelowcmb;0325 +ringcmb;030A +ringhalfleft;02BF +ringhalfleftarmenian;0559 +ringhalfleftbelowcmb;031C +ringhalfleftcentered;02D3 +ringhalfright;02BE +ringhalfrightbelowcmb;0339 +ringhalfrightcentered;02D2 +rinvertedbreve;0213 +rittorusquare;3351 +rlinebelow;1E5F +rlongleg;027C +rlonglegturned;027A +rmonospace;FF52 +rohiragana;308D +rokatakana;30ED +rokatakanahalfwidth;FF9B +roruathai;0E23 +rparen;24AD +rrabengali;09DC +rradeva;0931 +rragurmukhi;0A5C +rreharabic;0691 +rrehfinalarabic;FB8D +rrvocalicbengali;09E0 +rrvocalicdeva;0960 +rrvocalicgujarati;0AE0 +rrvocalicvowelsignbengali;09C4 +rrvocalicvowelsigndeva;0944 +rrvocalicvowelsigngujarati;0AC4 +rsuperior;F6F1 +rtblock;2590 +rturned;0279 +rturnedsuperior;02B4 +ruhiragana;308B +rukatakana;30EB +rukatakanahalfwidth;FF99 +rupeemarkbengali;09F2 +rupeesignbengali;09F3 +rupiah;F6DD +ruthai;0E24 +rvocalicbengali;098B +rvocalicdeva;090B +rvocalicgujarati;0A8B +rvocalicvowelsignbengali;09C3 +rvocalicvowelsigndeva;0943 +rvocalicvowelsigngujarati;0AC3 +s;0073 +sabengali;09B8 +sacute;015B +sacutedotaccent;1E65 +sadarabic;0635 +sadeva;0938 +sadfinalarabic;FEBA +sadinitialarabic;FEBB +sadmedialarabic;FEBC +sagujarati;0AB8 +sagurmukhi;0A38 +sahiragana;3055 +sakatakana;30B5 +sakatakanahalfwidth;FF7B +sallallahoualayhewasallamarabic;FDFA +samekh;05E1 +samekhdagesh;FB41 +samekhdageshhebrew;FB41 +samekhhebrew;05E1 +saraaathai;0E32 +saraaethai;0E41 +saraaimaimalaithai;0E44 +saraaimaimuanthai;0E43 +saraamthai;0E33 +saraathai;0E30 +saraethai;0E40 +saraiileftthai;F886 +saraiithai;0E35 +saraileftthai;F885 +saraithai;0E34 +saraothai;0E42 +saraueeleftthai;F888 +saraueethai;0E37 +saraueleftthai;F887 +sarauethai;0E36 +sarauthai;0E38 +sarauuthai;0E39 +sbopomofo;3119 +scaron;0161 +scarondotaccent;1E67 +scedilla;015F +schwa;0259 +schwacyrillic;04D9 +schwadieresiscyrillic;04DB +schwahook;025A +scircle;24E2 +scircumflex;015D +scommaaccent;0219 +sdotaccent;1E61 +sdotbelow;1E63 +sdotbelowdotaccent;1E69 +seagullbelowcmb;033C +second;2033 +secondtonechinese;02CA +section;00A7 +seenarabic;0633 +seenfinalarabic;FEB2 +seeninitialarabic;FEB3 +seenmedialarabic;FEB4 +segol;05B6 +segol13;05B6 +segol1f;05B6 +segol2c;05B6 +segolhebrew;05B6 +segolnarrowhebrew;05B6 +segolquarterhebrew;05B6 +segoltahebrew;0592 +segolwidehebrew;05B6 +seharmenian;057D +sehiragana;305B +sekatakana;30BB +sekatakanahalfwidth;FF7E +semicolon;003B +semicolonarabic;061B +semicolonmonospace;FF1B +semicolonsmall;FE54 +semivoicedmarkkana;309C +semivoicedmarkkanahalfwidth;FF9F +sentisquare;3322 +sentosquare;3323 +seven;0037 +sevenarabic;0667 +sevenbengali;09ED +sevencircle;2466 +sevencircleinversesansserif;2790 +sevendeva;096D +seveneighths;215E +sevengujarati;0AED +sevengurmukhi;0A6D +sevenhackarabic;0667 +sevenhangzhou;3027 +sevenideographicparen;3226 +seveninferior;2087 +sevenmonospace;FF17 +sevenoldstyle;F737 +sevenparen;247A +sevenperiod;248E +sevenpersian;06F7 +sevenroman;2176 +sevensuperior;2077 +seventeencircle;2470 +seventeenparen;2484 +seventeenperiod;2498 +seventhai;0E57 +sfthyphen;00AD +shaarmenian;0577 +shabengali;09B6 +shacyrillic;0448 +shaddaarabic;0651 +shaddadammaarabic;FC61 +shaddadammatanarabic;FC5E +shaddafathaarabic;FC60 +shaddafathatanarabic;0651 064B +shaddakasraarabic;FC62 +shaddakasratanarabic;FC5F +shade;2592 +shadedark;2593 +shadelight;2591 +shademedium;2592 +shadeva;0936 +shagujarati;0AB6 +shagurmukhi;0A36 +shalshelethebrew;0593 +shbopomofo;3115 +shchacyrillic;0449 +sheenarabic;0634 +sheenfinalarabic;FEB6 +sheeninitialarabic;FEB7 +sheenmedialarabic;FEB8 +sheicoptic;03E3 +sheqel;20AA +sheqelhebrew;20AA +sheva;05B0 +sheva115;05B0 +sheva15;05B0 +sheva22;05B0 +sheva2e;05B0 +shevahebrew;05B0 +shevanarrowhebrew;05B0 +shevaquarterhebrew;05B0 +shevawidehebrew;05B0 +shhacyrillic;04BB +shimacoptic;03ED +shin;05E9 +shindagesh;FB49 +shindageshhebrew;FB49 +shindageshshindot;FB2C +shindageshshindothebrew;FB2C +shindageshsindot;FB2D +shindageshsindothebrew;FB2D +shindothebrew;05C1 +shinhebrew;05E9 +shinshindot;FB2A +shinshindothebrew;FB2A +shinsindot;FB2B +shinsindothebrew;FB2B +shook;0282 +sigma;03C3 +sigma1;03C2 +sigmafinal;03C2 +sigmalunatesymbolgreek;03F2 +sihiragana;3057 +sikatakana;30B7 +sikatakanahalfwidth;FF7C +siluqhebrew;05BD +siluqlefthebrew;05BD +similar;223C +sindothebrew;05C2 +siosacirclekorean;3274 +siosaparenkorean;3214 +sioscieuckorean;317E +sioscirclekorean;3266 +sioskiyeokkorean;317A +sioskorean;3145 +siosnieunkorean;317B +siosparenkorean;3206 +siospieupkorean;317D +siostikeutkorean;317C +six;0036 +sixarabic;0666 +sixbengali;09EC +sixcircle;2465 +sixcircleinversesansserif;278F +sixdeva;096C +sixgujarati;0AEC +sixgurmukhi;0A6C +sixhackarabic;0666 +sixhangzhou;3026 +sixideographicparen;3225 +sixinferior;2086 +sixmonospace;FF16 +sixoldstyle;F736 +sixparen;2479 +sixperiod;248D +sixpersian;06F6 +sixroman;2175 +sixsuperior;2076 +sixteencircle;246F +sixteencurrencydenominatorbengali;09F9 +sixteenparen;2483 +sixteenperiod;2497 +sixthai;0E56 +slash;002F +slashmonospace;FF0F +slong;017F +slongdotaccent;1E9B +smileface;263A +smonospace;FF53 +sofpasuqhebrew;05C3 +softhyphen;00AD +softsigncyrillic;044C +sohiragana;305D +sokatakana;30BD +sokatakanahalfwidth;FF7F +soliduslongoverlaycmb;0338 +solidusshortoverlaycmb;0337 +sorusithai;0E29 +sosalathai;0E28 +sosothai;0E0B +sosuathai;0E2A +space;0020 +spacehackarabic;0020 +spade;2660 +spadesuitblack;2660 +spadesuitwhite;2664 +sparen;24AE +squarebelowcmb;033B +squarecc;33C4 +squarecm;339D +squarediagonalcrosshatchfill;25A9 +squarehorizontalfill;25A4 +squarekg;338F +squarekm;339E +squarekmcapital;33CE +squareln;33D1 +squarelog;33D2 +squaremg;338E +squaremil;33D5 +squaremm;339C +squaremsquared;33A1 +squareorthogonalcrosshatchfill;25A6 +squareupperlefttolowerrightfill;25A7 +squareupperrighttolowerleftfill;25A8 +squareverticalfill;25A5 +squarewhitewithsmallblack;25A3 +srsquare;33DB +ssabengali;09B7 +ssadeva;0937 +ssagujarati;0AB7 +ssangcieuckorean;3149 +ssanghieuhkorean;3185 +ssangieungkorean;3180 +ssangkiyeokkorean;3132 +ssangnieunkorean;3165 +ssangpieupkorean;3143 +ssangsioskorean;3146 +ssangtikeutkorean;3138 +ssuperior;F6F2 +sterling;00A3 +sterlingmonospace;FFE1 +strokelongoverlaycmb;0336 +strokeshortoverlaycmb;0335 +subset;2282 +subsetnotequal;228A +subsetorequal;2286 +succeeds;227B +suchthat;220B +suhiragana;3059 +sukatakana;30B9 +sukatakanahalfwidth;FF7D +sukunarabic;0652 +summation;2211 +sun;263C +superset;2283 +supersetnotequal;228B +supersetorequal;2287 +svsquare;33DC +syouwaerasquare;337C +t;0074 +tabengali;09A4 +tackdown;22A4 +tackleft;22A3 +tadeva;0924 +tagujarati;0AA4 +tagurmukhi;0A24 +taharabic;0637 +tahfinalarabic;FEC2 +tahinitialarabic;FEC3 +tahiragana;305F +tahmedialarabic;FEC4 +taisyouerasquare;337D +takatakana;30BF +takatakanahalfwidth;FF80 +tatweelarabic;0640 +tau;03C4 +tav;05EA +tavdages;FB4A +tavdagesh;FB4A +tavdageshhebrew;FB4A +tavhebrew;05EA +tbar;0167 +tbopomofo;310A +tcaron;0165 +tccurl;02A8 +tcedilla;0163 +tcheharabic;0686 +tchehfinalarabic;FB7B +tchehinitialarabic;FB7C +tchehmedialarabic;FB7D +tchehmeeminitialarabic;FB7C FEE4 +tcircle;24E3 +tcircumflexbelow;1E71 +tcommaaccent;0163 +tdieresis;1E97 +tdotaccent;1E6B +tdotbelow;1E6D +tecyrillic;0442 +tedescendercyrillic;04AD +teharabic;062A +tehfinalarabic;FE96 +tehhahinitialarabic;FCA2 +tehhahisolatedarabic;FC0C +tehinitialarabic;FE97 +tehiragana;3066 +tehjeeminitialarabic;FCA1 +tehjeemisolatedarabic;FC0B +tehmarbutaarabic;0629 +tehmarbutafinalarabic;FE94 +tehmedialarabic;FE98 +tehmeeminitialarabic;FCA4 +tehmeemisolatedarabic;FC0E +tehnoonfinalarabic;FC73 +tekatakana;30C6 +tekatakanahalfwidth;FF83 +telephone;2121 +telephoneblack;260E +telishagedolahebrew;05A0 +telishaqetanahebrew;05A9 +tencircle;2469 +tenideographicparen;3229 +tenparen;247D +tenperiod;2491 +tenroman;2179 +tesh;02A7 +tet;05D8 +tetdagesh;FB38 +tetdageshhebrew;FB38 +tethebrew;05D8 +tetsecyrillic;04B5 +tevirhebrew;059B +tevirlefthebrew;059B +thabengali;09A5 +thadeva;0925 +thagujarati;0AA5 +thagurmukhi;0A25 +thalarabic;0630 +thalfinalarabic;FEAC +thanthakhatlowleftthai;F898 +thanthakhatlowrightthai;F897 +thanthakhatthai;0E4C +thanthakhatupperleftthai;F896 +theharabic;062B +thehfinalarabic;FE9A +thehinitialarabic;FE9B +thehmedialarabic;FE9C +thereexists;2203 +therefore;2234 +theta;03B8 +theta1;03D1 +thetasymbolgreek;03D1 +thieuthacirclekorean;3279 +thieuthaparenkorean;3219 +thieuthcirclekorean;326B +thieuthkorean;314C +thieuthparenkorean;320B +thirteencircle;246C +thirteenparen;2480 +thirteenperiod;2494 +thonangmonthothai;0E11 +thook;01AD +thophuthaothai;0E12 +thorn;00FE +thothahanthai;0E17 +thothanthai;0E10 +thothongthai;0E18 +thothungthai;0E16 +thousandcyrillic;0482 +thousandsseparatorarabic;066C +thousandsseparatorpersian;066C +three;0033 +threearabic;0663 +threebengali;09E9 +threecircle;2462 +threecircleinversesansserif;278C +threedeva;0969 +threeeighths;215C +threegujarati;0AE9 +threegurmukhi;0A69 +threehackarabic;0663 +threehangzhou;3023 +threeideographicparen;3222 +threeinferior;2083 +threemonospace;FF13 +threenumeratorbengali;09F6 +threeoldstyle;F733 +threeparen;2476 +threeperiod;248A +threepersian;06F3 +threequarters;00BE +threequartersemdash;F6DE +threeroman;2172 +threesuperior;00B3 +threethai;0E53 +thzsquare;3394 +tihiragana;3061 +tikatakana;30C1 +tikatakanahalfwidth;FF81 +tikeutacirclekorean;3270 +tikeutaparenkorean;3210 +tikeutcirclekorean;3262 +tikeutkorean;3137 +tikeutparenkorean;3202 +tilde;02DC +tildebelowcmb;0330 +tildecmb;0303 +tildecomb;0303 +tildedoublecmb;0360 +tildeoperator;223C +tildeoverlaycmb;0334 +tildeverticalcmb;033E +timescircle;2297 +tipehahebrew;0596 +tipehalefthebrew;0596 +tippigurmukhi;0A70 +titlocyrilliccmb;0483 +tiwnarmenian;057F +tlinebelow;1E6F +tmonospace;FF54 +toarmenian;0569 +tohiragana;3068 +tokatakana;30C8 +tokatakanahalfwidth;FF84 +tonebarextrahighmod;02E5 +tonebarextralowmod;02E9 +tonebarhighmod;02E6 +tonebarlowmod;02E8 +tonebarmidmod;02E7 +tonefive;01BD +tonesix;0185 +tonetwo;01A8 +tonos;0384 +tonsquare;3327 +topatakthai;0E0F +tortoiseshellbracketleft;3014 +tortoiseshellbracketleftsmall;FE5D +tortoiseshellbracketleftvertical;FE39 +tortoiseshellbracketright;3015 +tortoiseshellbracketrightsmall;FE5E +tortoiseshellbracketrightvertical;FE3A +totaothai;0E15 +tpalatalhook;01AB +tparen;24AF +trademark;2122 +trademarksans;F8EA +trademarkserif;F6DB +tretroflexhook;0288 +triagdn;25BC +triaglf;25C4 +triagrt;25BA +triagup;25B2 +ts;02A6 +tsadi;05E6 +tsadidagesh;FB46 +tsadidageshhebrew;FB46 +tsadihebrew;05E6 +tsecyrillic;0446 +tsere;05B5 +tsere12;05B5 +tsere1e;05B5 +tsere2b;05B5 +tserehebrew;05B5 +tserenarrowhebrew;05B5 +tserequarterhebrew;05B5 +tserewidehebrew;05B5 +tshecyrillic;045B +tsuperior;F6F3 +ttabengali;099F +ttadeva;091F +ttagujarati;0A9F +ttagurmukhi;0A1F +tteharabic;0679 +ttehfinalarabic;FB67 +ttehinitialarabic;FB68 +ttehmedialarabic;FB69 +tthabengali;09A0 +tthadeva;0920 +tthagujarati;0AA0 +tthagurmukhi;0A20 +tturned;0287 +tuhiragana;3064 +tukatakana;30C4 +tukatakanahalfwidth;FF82 +tusmallhiragana;3063 +tusmallkatakana;30C3 +tusmallkatakanahalfwidth;FF6F +twelvecircle;246B +twelveparen;247F +twelveperiod;2493 +twelveroman;217B +twentycircle;2473 +twentyhangzhou;5344 +twentyparen;2487 +twentyperiod;249B +two;0032 +twoarabic;0662 +twobengali;09E8 +twocircle;2461 +twocircleinversesansserif;278B +twodeva;0968 +twodotenleader;2025 +twodotleader;2025 +twodotleadervertical;FE30 +twogujarati;0AE8 +twogurmukhi;0A68 +twohackarabic;0662 +twohangzhou;3022 +twoideographicparen;3221 +twoinferior;2082 +twomonospace;FF12 +twonumeratorbengali;09F5 +twooldstyle;F732 +twoparen;2475 +twoperiod;2489 +twopersian;06F2 +tworoman;2171 +twostroke;01BB +twosuperior;00B2 +twothai;0E52 +twothirds;2154 +u;0075 +uacute;00FA +ubar;0289 +ubengali;0989 +ubopomofo;3128 +ubreve;016D +ucaron;01D4 +ucircle;24E4 +ucircumflex;00FB +ucircumflexbelow;1E77 +ucyrillic;0443 +udattadeva;0951 +udblacute;0171 +udblgrave;0215 +udeva;0909 +udieresis;00FC +udieresisacute;01D8 +udieresisbelow;1E73 +udieresiscaron;01DA +udieresiscyrillic;04F1 +udieresisgrave;01DC +udieresismacron;01D6 +udotbelow;1EE5 +ugrave;00F9 +ugujarati;0A89 +ugurmukhi;0A09 +uhiragana;3046 +uhookabove;1EE7 +uhorn;01B0 +uhornacute;1EE9 +uhorndotbelow;1EF1 +uhorngrave;1EEB +uhornhookabove;1EED +uhorntilde;1EEF +uhungarumlaut;0171 +uhungarumlautcyrillic;04F3 +uinvertedbreve;0217 +ukatakana;30A6 +ukatakanahalfwidth;FF73 +ukcyrillic;0479 +ukorean;315C +umacron;016B +umacroncyrillic;04EF +umacrondieresis;1E7B +umatragurmukhi;0A41 +umonospace;FF55 +underscore;005F +underscoredbl;2017 +underscoremonospace;FF3F +underscorevertical;FE33 +underscorewavy;FE4F +union;222A +universal;2200 +uogonek;0173 +uparen;24B0 +upblock;2580 +upperdothebrew;05C4 +upsilon;03C5 +upsilondieresis;03CB +upsilondieresistonos;03B0 +upsilonlatin;028A +upsilontonos;03CD +uptackbelowcmb;031D +uptackmod;02D4 +uragurmukhi;0A73 +uring;016F +ushortcyrillic;045E +usmallhiragana;3045 +usmallkatakana;30A5 +usmallkatakanahalfwidth;FF69 +ustraightcyrillic;04AF +ustraightstrokecyrillic;04B1 +utilde;0169 +utildeacute;1E79 +utildebelow;1E75 +uubengali;098A +uudeva;090A +uugujarati;0A8A +uugurmukhi;0A0A +uumatragurmukhi;0A42 +uuvowelsignbengali;09C2 +uuvowelsigndeva;0942 +uuvowelsigngujarati;0AC2 +uvowelsignbengali;09C1 +uvowelsigndeva;0941 +uvowelsigngujarati;0AC1 +v;0076 +vadeva;0935 +vagujarati;0AB5 +vagurmukhi;0A35 +vakatakana;30F7 +vav;05D5 +vavdagesh;FB35 +vavdagesh65;FB35 +vavdageshhebrew;FB35 +vavhebrew;05D5 +vavholam;FB4B +vavholamhebrew;FB4B +vavvavhebrew;05F0 +vavyodhebrew;05F1 +vcircle;24E5 +vdotbelow;1E7F +vecyrillic;0432 +veharabic;06A4 +vehfinalarabic;FB6B +vehinitialarabic;FB6C +vehmedialarabic;FB6D +vekatakana;30F9 +venus;2640 +verticalbar;007C +verticallineabovecmb;030D +verticallinebelowcmb;0329 +verticallinelowmod;02CC +verticallinemod;02C8 +vewarmenian;057E +vhook;028B +vikatakana;30F8 +viramabengali;09CD +viramadeva;094D +viramagujarati;0ACD +visargabengali;0983 +visargadeva;0903 +visargagujarati;0A83 +vmonospace;FF56 +voarmenian;0578 +voicediterationhiragana;309E +voicediterationkatakana;30FE +voicedmarkkana;309B +voicedmarkkanahalfwidth;FF9E +vokatakana;30FA +vparen;24B1 +vtilde;1E7D +vturned;028C +vuhiragana;3094 +vukatakana;30F4 +w;0077 +wacute;1E83 +waekorean;3159 +wahiragana;308F +wakatakana;30EF +wakatakanahalfwidth;FF9C +wakorean;3158 +wasmallhiragana;308E +wasmallkatakana;30EE +wattosquare;3357 +wavedash;301C +wavyunderscorevertical;FE34 +wawarabic;0648 +wawfinalarabic;FEEE +wawhamzaabovearabic;0624 +wawhamzaabovefinalarabic;FE86 +wbsquare;33DD +wcircle;24E6 +wcircumflex;0175 +wdieresis;1E85 +wdotaccent;1E87 +wdotbelow;1E89 +wehiragana;3091 +weierstrass;2118 +wekatakana;30F1 +wekorean;315E +weokorean;315D +wgrave;1E81 +whitebullet;25E6 +whitecircle;25CB +whitecircleinverse;25D9 +whitecornerbracketleft;300E +whitecornerbracketleftvertical;FE43 +whitecornerbracketright;300F +whitecornerbracketrightvertical;FE44 +whitediamond;25C7 +whitediamondcontainingblacksmalldiamond;25C8 +whitedownpointingsmalltriangle;25BF +whitedownpointingtriangle;25BD +whiteleftpointingsmalltriangle;25C3 +whiteleftpointingtriangle;25C1 +whitelenticularbracketleft;3016 +whitelenticularbracketright;3017 +whiterightpointingsmalltriangle;25B9 +whiterightpointingtriangle;25B7 +whitesmallsquare;25AB +whitesmilingface;263A +whitesquare;25A1 +whitestar;2606 +whitetelephone;260F +whitetortoiseshellbracketleft;3018 +whitetortoiseshellbracketright;3019 +whiteuppointingsmalltriangle;25B5 +whiteuppointingtriangle;25B3 +wihiragana;3090 +wikatakana;30F0 +wikorean;315F +wmonospace;FF57 +wohiragana;3092 +wokatakana;30F2 +wokatakanahalfwidth;FF66 +won;20A9 +wonmonospace;FFE6 +wowaenthai;0E27 +wparen;24B2 +wring;1E98 +wsuperior;02B7 +wturned;028D +wynn;01BF +x;0078 +xabovecmb;033D +xbopomofo;3112 +xcircle;24E7 +xdieresis;1E8D +xdotaccent;1E8B +xeharmenian;056D +xi;03BE +xmonospace;FF58 +xparen;24B3 +xsuperior;02E3 +y;0079 +yaadosquare;334E +yabengali;09AF +yacute;00FD +yadeva;092F +yaekorean;3152 +yagujarati;0AAF +yagurmukhi;0A2F +yahiragana;3084 +yakatakana;30E4 +yakatakanahalfwidth;FF94 +yakorean;3151 +yamakkanthai;0E4E +yasmallhiragana;3083 +yasmallkatakana;30E3 +yasmallkatakanahalfwidth;FF6C +yatcyrillic;0463 +ycircle;24E8 +ycircumflex;0177 +ydieresis;00FF +ydotaccent;1E8F +ydotbelow;1EF5 +yeharabic;064A +yehbarreearabic;06D2 +yehbarreefinalarabic;FBAF +yehfinalarabic;FEF2 +yehhamzaabovearabic;0626 +yehhamzaabovefinalarabic;FE8A +yehhamzaaboveinitialarabic;FE8B +yehhamzaabovemedialarabic;FE8C +yehinitialarabic;FEF3 +yehmedialarabic;FEF4 +yehmeeminitialarabic;FCDD +yehmeemisolatedarabic;FC58 +yehnoonfinalarabic;FC94 +yehthreedotsbelowarabic;06D1 +yekorean;3156 +yen;00A5 +yenmonospace;FFE5 +yeokorean;3155 +yeorinhieuhkorean;3186 +yerahbenyomohebrew;05AA +yerahbenyomolefthebrew;05AA +yericyrillic;044B +yerudieresiscyrillic;04F9 +yesieungkorean;3181 +yesieungpansioskorean;3183 +yesieungsioskorean;3182 +yetivhebrew;059A +ygrave;1EF3 +yhook;01B4 +yhookabove;1EF7 +yiarmenian;0575 +yicyrillic;0457 +yikorean;3162 +yinyang;262F +yiwnarmenian;0582 +ymonospace;FF59 +yod;05D9 +yoddagesh;FB39 +yoddageshhebrew;FB39 +yodhebrew;05D9 +yodyodhebrew;05F2 +yodyodpatahhebrew;FB1F +yohiragana;3088 +yoikorean;3189 +yokatakana;30E8 +yokatakanahalfwidth;FF96 +yokorean;315B +yosmallhiragana;3087 +yosmallkatakana;30E7 +yosmallkatakanahalfwidth;FF6E +yotgreek;03F3 +yoyaekorean;3188 +yoyakorean;3187 +yoyakthai;0E22 +yoyingthai;0E0D +yparen;24B4 +ypogegrammeni;037A +ypogegrammenigreekcmb;0345 +yr;01A6 +yring;1E99 +ysuperior;02B8 +ytilde;1EF9 +yturned;028E +yuhiragana;3086 +yuikorean;318C +yukatakana;30E6 +yukatakanahalfwidth;FF95 +yukorean;3160 +yusbigcyrillic;046B +yusbigiotifiedcyrillic;046D +yuslittlecyrillic;0467 +yuslittleiotifiedcyrillic;0469 +yusmallhiragana;3085 +yusmallkatakana;30E5 +yusmallkatakanahalfwidth;FF6D +yuyekorean;318B +yuyeokorean;318A +yyabengali;09DF +yyadeva;095F +z;007A +zaarmenian;0566 +zacute;017A +zadeva;095B +zagurmukhi;0A5B +zaharabic;0638 +zahfinalarabic;FEC6 +zahinitialarabic;FEC7 +zahiragana;3056 +zahmedialarabic;FEC8 +zainarabic;0632 +zainfinalarabic;FEB0 +zakatakana;30B6 +zaqefgadolhebrew;0595 +zaqefqatanhebrew;0594 +zarqahebrew;0598 +zayin;05D6 +zayindagesh;FB36 +zayindageshhebrew;FB36 +zayinhebrew;05D6 +zbopomofo;3117 +zcaron;017E +zcircle;24E9 +zcircumflex;1E91 +zcurl;0291 +zdot;017C +zdotaccent;017C +zdotbelow;1E93 +zecyrillic;0437 +zedescendercyrillic;0499 +zedieresiscyrillic;04DF +zehiragana;305C +zekatakana;30BC +zero;0030 +zeroarabic;0660 +zerobengali;09E6 +zerodeva;0966 +zerogujarati;0AE6 +zerogurmukhi;0A66 +zerohackarabic;0660 +zeroinferior;2080 +zeromonospace;FF10 +zerooldstyle;F730 +zeropersian;06F0 +zerosuperior;2070 +zerothai;0E50 +zerowidthjoiner;FEFF +zerowidthnonjoiner;200C +zerowidthspace;200B +zeta;03B6 +zhbopomofo;3113 +zhearmenian;056A +zhebrevecyrillic;04C2 +zhecyrillic;0436 +zhedescendercyrillic;0497 +zhedieresiscyrillic;04DD +zihiragana;3058 +zikatakana;30B8 +zinorhebrew;05AE +zlinebelow;1E95 +zmonospace;FF5A +zohiragana;305E +zokatakana;30BE +zparen;24B5 +zretroflexhook;0290 +zstroke;01B6 +zuhiragana;305A +zukatakana;30BA +#--end diff --git a/src/main/resources/org/apache/xmlgraphics/fonts/zapfdingbats.txt b/src/main/resources/org/apache/xmlgraphics/fonts/zapfdingbats.txt new file mode 100644 index 0000000..2b8cd5a --- /dev/null +++ b/src/main/resources/org/apache/xmlgraphics/fonts/zapfdingbats.txt @@ -0,0 +1,243 @@ +# ################################################################################### +# Copyright (c) 1997,1998,2002,2007 Adobe Systems Incorporated +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this documentation file to use, copy, publish, distribute, +# sublicense, and/or sell copies of the documentation, and to permit +# others to do the same, provided that: +# - No modification, editing or other alteration of this document is +# allowed; and +# - The above copyright notice and this permission notice shall be +# included in all copies of the documentation. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this documentation file, to create their own derivative works +# from the content of this document to use, copy, publish, distribute, +# sublicense, and/or sell the derivative works, and to permit others to do +# the same, provided that the derived work is not represented as being a +# copy or version of this document. +# +# Adobe shall not be liable to any party for any loss of revenue or profit +# or for indirect, incidental, special, consequential, or other similar +# damages, whether based on tort (including without limitation negligence +# or strict liability), contract or other legal or equitable grounds even +# if Adobe has been advised or had reason to know of the possibility of +# such damages. The Adobe materials are provided on an "AS IS" basis. +# Adobe specifically disclaims all express, statutory, or implied +# warranties relating to the Adobe materials, including but not limited to +# those concerning merchantability or fitness for a particular purpose or +# non-infringement of any third party rights regarding the Adobe +# materials. +# ################################################################################### +# Name: ITC Zapf Dingbats Glyph List +# Table version: 2.0 +# Date: September 20, 2002 +# +# See http://partners.adobe.com/asn/developer/typeforum/unicodegn.html +# +# Format: Semicolon-delimited fields: +# (1) glyph name +# (2) Unicode scalar value +# +a100;275E +a101;2761 +a102;2762 +a103;2763 +a104;2764 +a105;2710 +a106;2765 +a107;2766 +a108;2767 +a109;2660 +a10;2721 +a110;2665 +a111;2666 +a112;2663 +a117;2709 +a118;2708 +a119;2707 +a11;261B +a120;2460 +a121;2461 +a122;2462 +a123;2463 +a124;2464 +a125;2465 +a126;2466 +a127;2467 +a128;2468 +a129;2469 +a12;261E +a130;2776 +a131;2777 +a132;2778 +a133;2779 +a134;277A +a135;277B +a136;277C +a137;277D +a138;277E +a139;277F +a13;270C +a140;2780 +a141;2781 +a142;2782 +a143;2783 +a144;2784 +a145;2785 +a146;2786 +a147;2787 +a148;2788 +a149;2789 +a14;270D +a150;278A +a151;278B +a152;278C +a153;278D +a154;278E +a155;278F +a156;2790 +a157;2791 +a158;2792 +a159;2793 +a15;270E +a160;2794 +a161;2192 +a162;27A3 +a163;2194 +a164;2195 +a165;2799 +a166;279B +a167;279C +a168;279D +a169;279E +a16;270F +a170;279F +a171;27A0 +a172;27A1 +a173;27A2 +a174;27A4 +a175;27A5 +a176;27A6 +a177;27A7 +a178;27A8 +a179;27A9 +a17;2711 +a180;27AB +a181;27AD +a182;27AF +a183;27B2 +a184;27B3 +a185;27B5 +a186;27B8 +a187;27BA +a188;27BB +a189;27BC +a18;2712 +a190;27BD +a191;27BE +a192;279A +a193;27AA +a194;27B6 +a195;27B9 +a196;2798 +a197;27B4 +a198;27B7 +a199;27AC +a19;2713 +a1;2701 +a200;27AE +a201;27B1 +a202;2703 +a203;2750 +a204;2752 +a205;276E +a206;2770 +a20;2714 +a21;2715 +a22;2716 +a23;2717 +a24;2718 +a25;2719 +a26;271A +a27;271B +a28;271C +a29;2722 +a2;2702 +a30;2723 +a31;2724 +a32;2725 +a33;2726 +a34;2727 +a35;2605 +a36;2729 +a37;272A +a38;272B +a39;272C +a3;2704 +a40;272D +a41;272E +a42;272F +a43;2730 +a44;2731 +a45;2732 +a46;2733 +a47;2734 +a48;2735 +a49;2736 +a4;260E +a50;2737 +a51;2738 +a52;2739 +a53;273A +a54;273B +a55;273C +a56;273D +a57;273E +a58;273F +a59;2740 +a5;2706 +a60;2741 +a61;2742 +a62;2743 +a63;2744 +a64;2745 +a65;2746 +a66;2747 +a67;2748 +a68;2749 +a69;274A +a6;271D +a70;274B +a71;25CF +a72;274D +a73;25A0 +a74;274F +a75;2751 +a76;25B2 +a77;25BC +a78;25C6 +a79;2756 +a7;271E +a81;25D7 +a82;2758 +a83;2759 +a84;275A +a85;276F +a86;2771 +a87;2772 +a88;2773 +a89;2768 +a8;271F +a90;2769 +a91;276C +a92;276D +a93;276A +a94;276B +a95;2774 +a96;2775 +a97;275B +a98;275C +a99;275D +a9;2720 +#-- end diff --git a/src/main/resources/org/apache/xmlgraphics/image/codec/Messages.properties b/src/main/resources/org/apache/xmlgraphics/image/codec/Messages.properties new file mode 100644 index 0000000..ee208a2 --- /dev/null +++ b/src/main/resources/org/apache/xmlgraphics/image/codec/Messages.properties @@ -0,0 +1,158 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +FileCacheSeekableStream0=pos < 0. +FileSeekableStream0=pos < 0. +FloatDoubleColorModel0=transferType must be DataBuffer.TYPE_FLOAT or DataBuffer.TYPE_DOUBLE. +FloatDoubleColorModel10=elements required in the components array. +FloatDoubleColorModel1=getRed(int) not supported by this ColorModel. +FloatDoubleColorModel2=getGreen(int) not supported by this ColorModel. +FloatDoubleColorModel3=getBlue(int) not supported by this ColorModel. +FloatDoubleColorModel4=getAlpha(int) not supported by this ColorModel. +FloatDoubleColorModel5=getRGB(int) not supported by this ColorModel. +FloatDoubleColorModel6=raster transfer type must match that of this ColorModel. +FloatDoubleColorModel7=Type of pixel does not match transfer type. +FloatDoubleColorModel8=pixel array is not large enough to hold all color/alpha components. +FloatDoubleColorModel9=Pixel values for FloatDoubleColorModel cannot be represented as a single integer. +MemoryCacheSeekableStream0=pos < 0. +PNGCodec0=PNG encoding not supported yet. +PNGDecodeParam0=User exponent must not be negative. +PNGDecodeParam1=Display exponent must not be negative. +PNGEncodeParam0=Bad palette length. +PNGEncodeParam10=Transparent RGB value has not been set. +PNGEncodeParam11=Grayscale bit depth has not been set. +PNGEncodeParam12=Chromaticity has not been set. +PNGEncodeParam13=Gamma has not been set. +PNGEncodeParam14=Palette histogram has not been set. +PNGEncodeParam15=ICC profile has not been set. +PNGEncodeParam16=Physical dimension information has not been set. +PNGEncodeParam17=Suggested palette information has not been set. +PNGEncodeParam18=Significant bits values have not been set. +PNGEncodeParam19=sRGB rendering intent has not been set. +PNGEncodeParam1=Not divisible by 3. +PNGEncodeParam20=Uncompressed text strings have not been set. +PNGEncodeParam21=Modification time has not been set. +PNGEncodeParam22=Compressed text strings have not been set. +PNGEncodeParam23='unsetBackground' not implemented by the superclass 'PNGEncodeParam'. +PNGEncodeParam24='isBackgroundSet' not implemented by the superclass 'PNGEncodeParam'. +PNGEncodeParam25=Bit shift must be greater than 0. +PNGEncodeParam26=Bit depth must be 8 or 16. +PNGEncodeParam27=RGB value must have three components. +PNGEncodeParam28=Chromaticity array must be non-empty. +PNGEncodeParam2=Bit depth not equal to 1, 2, 4, or 8. +PNGEncodeParam3=RGB palette has not been set. +PNGEncodeParam4=background palette index has not been set. +PNGEncodeParam5=Palette transparency has not been set. +PNGEncodeParam6=Background gray level has not been set. +PNGEncodeParam7=Transparent gray value has not been set. +PNGEncodeParam8=Bit shift has not been set. +PNGEncodeParam9=RGB background color has not been set. +PNGImageDecoder0=PNG magic number not found. +PNGImageDecoder10=Unsupported PNG filter method (not 0). +PNGImageDecoder11=Unsupported PNG interlace method (not 0 or 1). +PNGImageDecoder12=Unknown PNG pHYs unit specifier (not 0 or 1). +PNGImageDecoder13=Illegal PNG sBit value (< 0 or > bit depth). +PNGImageDecoder14=Too many PNG alpha palette entries. +PNGImageDecoder15=PNG image already has alpha, can't have tRNS chunk. +PNGImageDecoder16=Unknown PNG filter type (not 0-4). +PNGImageDecoder17=Illegal tile requested from a PNG image. +PNGImageDecoder18=PNG can't have hIST chunk without a PLTE chunk. +PNGImageDecoder19=Illegal page requested from a PNG file. +PNGImageDecoder1=Error reading PNG header. +PNGImageDecoder2=I/O error reading PNG file. +PNGImageDecoder3=Illegal bit depth for a PNG image. +PNGImageDecoder4=Bad color type for a PNG image. +PNGImageDecoder5=An RGB PNG image can't have a bit depth less than 8. +PNGImageDecoder6=A palette-color PNG image can't have a bit depth of 16. +PNGImageDecoder7=A PNG Gray+Alpha image can't have a bit depth less than 8. +PNGImageDecoder8=A PNG RGB+Alpha image can't have a bit depth less than 8. +PNGImageDecoder9=Unsupported PNG compression method (not 0). +PNGImageEncoder0=Sample size not equal to bit depth. +PNGImageEncoder1=Bit depth greater than 16. +PNGImageEncoder2=Bit depth less than 1 or greater than 8. +PNGImageEncoder3=Number of bands not equal to 1. +PNGImageEncoder4=PNG encode parameter must be Palette or Gray. +PropertySet0=Not implemented. +RasterFactory0=Number of bands must be greater than 0. +RasterFactory10=parentY lies outside raster. +RasterFactory11=(parentX + width) is outside raster. +RasterFactory12=(parentY + height) is outside raster. +RasterFactory13=Illegal value for transparency. +RasterFactory14=Transparency cannot be opaque when useAlpha is true. +RasterFactory15=bitsPerBands must be greater than 0. +RasterFactory16=Size of array must be smaller than Integer.MAX_VALUE. +RasterFactory1=Bank indices array is null. +RasterFactory2=bankIndices.length != bandOffsets.length +RasterFactory3=Unsupported data type. +RasterFactory4=Band offsets array is null. +RasterFactory5=Offsets between bands must be less than the scanline stride. +RasterFactory6=Pixel stride times width must be less than the scanline stride. +RasterFactory7=Pixel stride must be greater than or equal to the offset between bands. +RasterFactory8=This method does not support the input data type. +RasterFactory9=parentX lies outside raster. +SegmentedSeekableStream0=Source stream does not support seeking backwards. +SingleTileRenderedImage0=Illegal tile requested from a SingleTileRenderedImage. +TIFFImage0=Planar (band-sequential) format TIFF is not supported. +TIFFImage1=All samples must have the same bit depth. +TIFFImage2=All samples must have the same data format. +TIFFImage3=Unsupported combination of bit depth and sample format. +TIFFImage4=Unsupported image type. +TIFFImage5=Strip offsets, a required field, is not present in the TIFF file. +TIFFImage5=Strip byte counts, a required field, is not present in the TIFF file. +TIFFImage7=Unsupported compression type for non-bilevel data. +TIFFImage8=Illegal value for predictor in TIFF file. +TIFFImage9=Sample size must be 8 for horizontal differencing predictor. +TIFFImage10=Unsupported compression type +TIFFImage11=Colormap must be present for a Palette Color image. +TIFFImage12=Illegal tile requested from a TIFFImage. +TIFFImage13=IOException occured while reading TIFF image data +TIFFImage14=Unable to decode packbits compressed data - not enough data +TIFFImage15=Decoding of old style JPEG-in-TIFF data is not supported. +TIFFImage17=Error inflating data +TIFFImage18=Unsupported field type +TIFFImage19=Unsupported number of bands +TIFFImage20=Unsupported data type +TIFFImageDecoder0=Illegal page requested from a TIFF file. +TIFFImageEncoder0=All samples must have the same bit depth. +TIFFImageEncoder1=1- and 4-bit data supported for single band images only. +TIFFImageEncoder2=Byte buffers require 1-, 4-, or b-bit data. +TIFFImageEncoder3=Short or unsigned short buffers require 16-bit data. +TIFFImageEncoder4=Int or float buffers require 32-bit data. +TIFFImageEncoder5=Unsupported output data type. +TIFFImageEncoder6=TIFF encoder does not support (unsigned) short palette images. +TIFFImageEncoder7=Invalid image - An image with sampleSize of 1 bit must have IndexColorModel with mapsize of 2. +TIFFImageEncoder8=Image type not supported for output. +TIFFImageEncoder9=JPEG-in-TIFF encoding supported only for 8-bit samples and either 1 (grayscale) or 3 (RGB or YCbCr) samples per pixel. +TIFFImageEncoder10=Unsupported TIFFField type. +TIFFImageEncoder11=Extra images may not be used when encoding multiple page file. +TIFFImageEncoder12=JPEG compression not supported. +TIFFImageEncoder13=No output specified. +TIFFLZWDecoder0=TIFF 5.0 LZW codes are not supported. +TIFFFaxDecoder0=ERROR code word (0) encountered. +TIFFFaxDecoder1=EOL code word (15) encountered in White run. +TIFFFaxDecoder2=EOL code word (15) encountered in Black run. +TIFFFaxDecoder3=First scanline must be 1D encoded. +TIFFFaxDecoder4=Invalid code encountered while decoding 2D group 3 compressed data. +TIFFFaxDecoder5=Invalid code encountered while decoding 2D group 4 compressed data. +TIFFFaxDecoder6=Scanline must begin with EOL code word. +TIFFFaxDecoder7=TIFF_FILL_ORDER tag must be either 1 or 2. +TIFFFaxDecoder8=All fill bits preceding EOL code must be 0. +TIFFDirectory0=Unsupported TIFFField tag. +TIFFDirectory1=Bad endianness tag (not 0x4949 or 0x4d4d). +TIFFDirectory2=Bad magic number, should be 42. +TIFFDirectory3=Directory number too large. +TIFFEncodeParam0=Unsupported compression scheme specified. +TIFFEncodeParam1=Illegal DEFLATE compression level specified. \ No newline at end of file diff --git a/src/main/resources/org/apache/xmlgraphics/image/writer/default-preferred-order.properties b/src/main/resources/org/apache/xmlgraphics/image/writer/default-preferred-order.properties new file mode 100644 index 0000000..1bcbf16 --- /dev/null +++ b/src/main/resources/org/apache/xmlgraphics/image/writer/default-preferred-order.properties @@ -0,0 +1,18 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +org.apache.xmlgraphics.image.writer.internal=-2000 +org.apache.xmlgraphics.image.writer.imageio=-1000 \ No newline at end of file diff --git a/src/main/resources/org/apache/xmlgraphics/ps/Identity-H b/src/main/resources/org/apache/xmlgraphics/ps/Identity-H new file mode 100644 index 0000000..6675416 --- /dev/null +++ b/src/main/resources/org/apache/xmlgraphics/ps/Identity-H @@ -0,0 +1,339 @@ +%!PS-Adobe-3.0 Resource-CMap +%%DocumentNeededResources: ProcSet (CIDInit) +%%IncludeResource: ProcSet (CIDInit) +%%BeginResource: CMap (Identity-H) +%%Title: (Identity-H Adobe Identity 0) +%%Version: 10.003 +%%Copyright: ----------------------------------------------------------- +%%Copyright: Copyright 1990-2009 Adobe Systems Incorporated. +%%Copyright: All rights reserved. +%%Copyright: +%%Copyright: Redistribution and use in source and binary forms, with or +%%Copyright: without modification, are permitted provided that the +%%Copyright: following conditions are met: +%%Copyright: +%%Copyright: Redistributions of source code must retain the above +%%Copyright: copyright notice, this list of conditions and the following +%%Copyright: disclaimer. +%%Copyright: +%%Copyright: Redistributions in binary form must reproduce the above +%%Copyright: copyright notice, this list of conditions and the following +%%Copyright: disclaimer in the documentation and/or other materials +%%Copyright: provided with the distribution. +%%Copyright: +%%Copyright: Neither the name of Adobe Systems Incorporated nor the names +%%Copyright: of its contributors may be used to endorse or promote +%%Copyright: products derived from this software without specific prior +%%Copyright: written permission. +%%Copyright: +%%Copyright: THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +%%Copyright: CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +%%Copyright: INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +%%Copyright: MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +%%Copyright: DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +%%Copyright: CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +%%Copyright: SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +%%Copyright: NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +%%Copyright: LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +%%Copyright: HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +%%Copyright: CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +%%Copyright: OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +%%Copyright: SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +%%Copyright: ----------------------------------------------------------- +%%EndComments + +/CIDInit /ProcSet findresource begin + +12 dict begin + +begincmap + +/CIDSystemInfo 3 dict dup begin + /Registry (Adobe) def + /Ordering (Identity) def + /Supplement 0 def +end def + +/CMapName /Identity-H def +/CMapVersion 10.003 def +/CMapType 1 def + +/XUID [1 10 25404 9999] def + +/WMode 0 def + +1 begincodespacerange + <0000> +endcodespacerange + +100 begincidrange +<0000> <00ff> 0 +<0100> <01ff> 256 +<0200> <02ff> 512 +<0300> <03ff> 768 +<0400> <04ff> 1024 +<0500> <05ff> 1280 +<0600> <06ff> 1536 +<0700> <07ff> 1792 +<0800> <08ff> 2048 +<0900> <09ff> 2304 +<0a00> <0aff> 2560 +<0b00> <0bff> 2816 +<0c00> <0cff> 3072 +<0d00> <0dff> 3328 +<0e00> <0eff> 3584 +<0f00> <0fff> 3840 +<1000> <10ff> 4096 +<1100> <11ff> 4352 +<1200> <12ff> 4608 +<1300> <13ff> 4864 +<1400> <14ff> 5120 +<1500> <15ff> 5376 +<1600> <16ff> 5632 +<1700> <17ff> 5888 +<1800> <18ff> 6144 +<1900> <19ff> 6400 +<1a00> <1aff> 6656 +<1b00> <1bff> 6912 +<1c00> <1cff> 7168 +<1d00> <1dff> 7424 +<1e00> <1eff> 7680 +<1f00> <1fff> 7936 +<2000> <20ff> 8192 +<2100> <21ff> 8448 +<2200> <22ff> 8704 +<2300> <23ff> 8960 +<2400> <24ff> 9216 +<2500> <25ff> 9472 +<2600> <26ff> 9728 +<2700> <27ff> 9984 +<2800> <28ff> 10240 +<2900> <29ff> 10496 +<2a00> <2aff> 10752 +<2b00> <2bff> 11008 +<2c00> <2cff> 11264 +<2d00> <2dff> 11520 +<2e00> <2eff> 11776 +<2f00> <2fff> 12032 +<3000> <30ff> 12288 +<3100> <31ff> 12544 +<3200> <32ff> 12800 +<3300> <33ff> 13056 +<3400> <34ff> 13312 +<3500> <35ff> 13568 +<3600> <36ff> 13824 +<3700> <37ff> 14080 +<3800> <38ff> 14336 +<3900> <39ff> 14592 +<3a00> <3aff> 14848 +<3b00> <3bff> 15104 +<3c00> <3cff> 15360 +<3d00> <3dff> 15616 +<3e00> <3eff> 15872 +<3f00> <3fff> 16128 +<4000> <40ff> 16384 +<4100> <41ff> 16640 +<4200> <42ff> 16896 +<4300> <43ff> 17152 +<4400> <44ff> 17408 +<4500> <45ff> 17664 +<4600> <46ff> 17920 +<4700> <47ff> 18176 +<4800> <48ff> 18432 +<4900> <49ff> 18688 +<4a00> <4aff> 18944 +<4b00> <4bff> 19200 +<4c00> <4cff> 19456 +<4d00> <4dff> 19712 +<4e00> <4eff> 19968 +<4f00> <4fff> 20224 +<5000> <50ff> 20480 +<5100> <51ff> 20736 +<5200> <52ff> 20992 +<5300> <53ff> 21248 +<5400> <54ff> 21504 +<5500> <55ff> 21760 +<5600> <56ff> 22016 +<5700> <57ff> 22272 +<5800> <58ff> 22528 +<5900> <59ff> 22784 +<5a00> <5aff> 23040 +<5b00> <5bff> 23296 +<5c00> <5cff> 23552 +<5d00> <5dff> 23808 +<5e00> <5eff> 24064 +<5f00> <5fff> 24320 +<6000> <60ff> 24576 +<6100> <61ff> 24832 +<6200> <62ff> 25088 +<6300> <63ff> 25344 +endcidrange + +100 begincidrange +<6400> <64ff> 25600 +<6500> <65ff> 25856 +<6600> <66ff> 26112 +<6700> <67ff> 26368 +<6800> <68ff> 26624 +<6900> <69ff> 26880 +<6a00> <6aff> 27136 +<6b00> <6bff> 27392 +<6c00> <6cff> 27648 +<6d00> <6dff> 27904 +<6e00> <6eff> 28160 +<6f00> <6fff> 28416 +<7000> <70ff> 28672 +<7100> <71ff> 28928 +<7200> <72ff> 29184 +<7300> <73ff> 29440 +<7400> <74ff> 29696 +<7500> <75ff> 29952 +<7600> <76ff> 30208 +<7700> <77ff> 30464 +<7800> <78ff> 30720 +<7900> <79ff> 30976 +<7a00> <7aff> 31232 +<7b00> <7bff> 31488 +<7c00> <7cff> 31744 +<7d00> <7dff> 32000 +<7e00> <7eff> 32256 +<7f00> <7fff> 32512 +<8000> <80ff> 32768 +<8100> <81ff> 33024 +<8200> <82ff> 33280 +<8300> <83ff> 33536 +<8400> <84ff> 33792 +<8500> <85ff> 34048 +<8600> <86ff> 34304 +<8700> <87ff> 34560 +<8800> <88ff> 34816 +<8900> <89ff> 35072 +<8a00> <8aff> 35328 +<8b00> <8bff> 35584 +<8c00> <8cff> 35840 +<8d00> <8dff> 36096 +<8e00> <8eff> 36352 +<8f00> <8fff> 36608 +<9000> <90ff> 36864 +<9100> <91ff> 37120 +<9200> <92ff> 37376 +<9300> <93ff> 37632 +<9400> <94ff> 37888 +<9500> <95ff> 38144 +<9600> <96ff> 38400 +<9700> <97ff> 38656 +<9800> <98ff> 38912 +<9900> <99ff> 39168 +<9a00> <9aff> 39424 +<9b00> <9bff> 39680 +<9c00> <9cff> 39936 +<9d00> <9dff> 40192 +<9e00> <9eff> 40448 +<9f00> <9fff> 40704 + 40960 + 41216 + 41472 + 41728 + 41984 + 42240 + 42496 + 42752 + 43008 + 43264 + 43520 + 43776 + 44032 + 44288 + 44544 + 44800 + 45056 + 45312 + 45568 + 45824 + 46080 + 46336 + 46592 + 46848 + 47104 + 47360 + 47616 + 47872 + 48128 + 48384 + 48640 + 48896 + 49152 + 49408 + 49664 + 49920 + 50176 + 50432 + 50688 + 50944 +endcidrange + +56 begincidrange + 51200 + 51456 + 51712 + 51968 + 52224 + 52480 + 52736 + 52992 + 53248 + 53504 + 53760 + 54016 + 54272 + 54528 + 54784 + 55040 + 55296 + 55552 + 55808 + 56064 + 56320 + 56576 + 56832 + 57088 + 57344 + 57600 + 57856 + 58112 + 58368 + 58624 + 58880 + 59136 + 59392 + 59648 + 59904 + 60160 + 60416 + 60672 + 60928 + 61184 + 61440 + 61696 + 61952 + 62208 + 62464 + 62720 + 62976 + 63232 + 63488 + 63744 + 64000 + 64256 + 64512 + 64768 + 65024 + 65280 +endcidrange +endcmap +CMapName currentdict /CMap defineresource pop +end +end + +%%EndResource +%%EOF diff --git a/src/test/java/org/apache/xmlgraphics/StandardTestSuite.java b/src/test/java/org/apache/xmlgraphics/StandardTestSuite.java new file mode 100644 index 0000000..202089e --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/StandardTestSuite.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: StandardTestSuite.java 1365650 2012-07-25 15:59:30Z mehdi $ */ + +package org.apache.xmlgraphics; + +import org.junit.runner.RunWith; +import org.junit.runners.Suite; +import org.junit.runners.Suite.SuiteClasses; + +import org.apache.xmlgraphics.image.codec.png.PNGEncoderTestCase; +import org.apache.xmlgraphics.ps.ImageEncodingHelperTestCase; +import org.apache.xmlgraphics.ps.PSEscapeTestCase; +import org.apache.xmlgraphics.ps.dsc.ListenerTestCase; +import org.apache.xmlgraphics.ps.dsc.events.DSCValueParserTestCase; +import org.apache.xmlgraphics.ps.dsc.tools.DSCToolsTestCase; +import org.apache.xmlgraphics.util.ClasspathResourceTestCase; +import org.apache.xmlgraphics.util.ServiceTestCase; +import org.apache.xmlgraphics.util.UnitConvTestCase; +import org.apache.xmlgraphics.util.io.ASCII85InputStreamTestCase; +import org.apache.xmlgraphics.util.io.ASCII85OutputStreamTestCase; +import org.apache.xmlgraphics.util.io.Base64TestCase; + +/** + * Test suite for basic functionality of XML Graphics Commons. + */ +@RunWith(Suite.class) +@SuiteClasses({ + Base64TestCase.class, + ASCII85InputStreamTestCase.class, + ASCII85OutputStreamTestCase.class, + PNGEncoderTestCase.class, + ServiceTestCase.class, + ClasspathResourceTestCase.class, + PSEscapeTestCase.class, + ImageEncodingHelperTestCase.class, + DSCValueParserTestCase.class, + DSCToolsTestCase.class, + ListenerTestCase.class, + UnitConvTestCase.class +}) +public class StandardTestSuite { +} diff --git a/src/test/java/org/apache/xmlgraphics/fonts/GlyphsTestCase.java b/src/test/java/org/apache/xmlgraphics/fonts/GlyphsTestCase.java new file mode 100644 index 0000000..8595e7a --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/fonts/GlyphsTestCase.java @@ -0,0 +1,98 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: GlyphsTestCase.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.fonts; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +/** + * Tests for the Glyphs class. + */ +public class GlyphsTestCase { + + @Test + public void testGetUnicodeSequenceForGlyphName() throws Exception { + String glyph; + String unicodes; + + glyph = "Omega"; + unicodes = Glyphs.getUnicodeSequenceForGlyphName(glyph); + assertEquals(1, unicodes.length()); + assertEquals("Must contain 2126 - OHM SIGN", + unicodes.charAt(0), '\u2126'); + + glyph = "Omegagreek"; + unicodes = Glyphs.getUnicodeSequenceForGlyphName(glyph); + assertEquals(1, unicodes.length()); + assertEquals("Must contain 03A9 - GREEK CAPITAL LETTER OMEGA", + unicodes.charAt(0), '\u03A9'); + + glyph = "A"; + unicodes = Glyphs.getUnicodeSequenceForGlyphName(glyph); + assertEquals(1, unicodes.length()); + assertEquals("Must contain 0041 - LATIN CAPITAL LETTER A", + unicodes.charAt(0), '\u0041'); + + glyph = "rehyehaleflamarabic"; + unicodes = Glyphs.getUnicodeSequenceForGlyphName(glyph); + assertEquals(4, unicodes.length()); + assertEquals("Expected 0631 - ARABIC LETTER REH at position 0", + unicodes.charAt(0), '\u0631'); + assertEquals("Expected FEF3 - ARABIC LETTER YEH INITAL FORM at position 1", + unicodes.charAt(1), '\uFEF3'); + assertEquals("Expected FE8E - ARABIC LETTER ALEF FINAL FORM at position 2", + unicodes.charAt(2), '\uFE8E'); + assertEquals("Expected 0644 - ARABIC LETTER LAM at position 3", + unicodes.charAt(3), '\u0644'); + + glyph = "Lcommaaccent_uni20AC0308_u20AC"; + unicodes = Glyphs.getUnicodeSequenceForGlyphName(glyph); + assertEquals(4, unicodes.length()); + assertEquals("Must contain 013B", + unicodes.charAt(0), '\u013B'); + assertEquals("Must contain 20AC", + unicodes.charAt(1), '\u20AC'); + assertEquals("Must contain 0308", + unicodes.charAt(2), '\u0308'); + assertEquals("Must contain 20AC", + unicodes.charAt(3), '\u20AC'); + + glyph = "blah"; + unicodes = Glyphs.getUnicodeSequenceForGlyphName(glyph); + assertNull(unicodes); + } + + @Test + public void testGetCharNameAlternativesFor() throws Exception { + String[] alts = Glyphs.getCharNameAlternativesFor("Omega"); + assertEquals(1, alts.length); + assertEquals("Omegagreek", alts[0]); + + alts = Glyphs.getCharNameAlternativesFor("nbspace"); + assertEquals(2, alts.length); + assertEquals("space", alts[0]); + assertEquals("nonbreakingspace", alts[1]); + + alts = Glyphs.getCharNameAlternativesFor("A"); + assertNull(alts); + } +} diff --git a/src/test/java/org/apache/xmlgraphics/image/codec/png/CodecResourcesTestCase.java b/src/test/java/org/apache/xmlgraphics/image/codec/png/CodecResourcesTestCase.java new file mode 100644 index 0000000..eb284a9 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/image/codec/png/CodecResourcesTestCase.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: CodecResourcesTestCase.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.image.codec.png; + +import java.io.InputStream; + +import org.junit.Test; + +import static org.junit.Assert.fail; + +import org.apache.commons.io.IOUtils; + +import org.apache.xmlgraphics.image.codec.util.MemoryCacheSeekableStream; +import org.apache.xmlgraphics.image.codec.util.SeekableStream; + +/** + * Checks for the presence of message resources for the internal codecs. + */ +public class CodecResourcesTestCase { + + @Test + public void testResources() throws Exception { + + InputStream in = new java.io.FileInputStream("test/images/barcode.eps"); + SeekableStream seekStream = new MemoryCacheSeekableStream(in); + try { + new PNGImage(seekStream, null); + fail("Exception expected"); + } catch (RuntimeException re) { + String msg = re.getMessage(); + if ("PNGImageDecoder0".equals(msg)) { + re.printStackTrace(); + fail("Message resource don't seem to be present! Message is: " + msg); + } else if (msg.toLowerCase().indexOf("magic") < 0) { + fail("Message not as expected! Message is: " + msg); + } + } finally { + IOUtils.closeQuietly(seekStream); + IOUtils.closeQuietly(in); + } + } +} diff --git a/src/test/java/org/apache/xmlgraphics/image/codec/png/PNGEncoderTestCase.java b/src/test/java/org/apache/xmlgraphics/image/codec/png/PNGEncoderTestCase.java new file mode 100644 index 0000000..db85632 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/image/codec/png/PNGEncoderTestCase.java @@ -0,0 +1,155 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PNGEncoderTestCase.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.image.codec.png; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.geom.AffineTransform; +import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; +import java.awt.image.RenderedImage; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.OutputStream; + +import org.junit.Test; + +import static org.junit.Assert.fail; + +/** + * This test validates the PNGEncoder operation. It creates a + * BufferedImage, then encodes it with the PNGEncoder, then + * decodes it and compares the decoded image with the original one. + * + * @version $Id: PNGEncoderTestCase.java 1732018 2016-02-24 04:51:06Z gadams $ + */ +public class PNGEncoderTestCase { + + @Test + public void testPNGEncoding() throws Exception { + // Create a BufferedImage to be encoded + BufferedImage image = new BufferedImage(100, 75, BufferedImage.TYPE_INT_ARGB); + Graphics2D ig = image.createGraphics(); + ig.scale(.5, .5); + ig.setPaint(new Color(128, 0, 0)); + ig.fillRect(0, 0, 100, 50); + ig.setPaint(Color.orange); + ig.fillRect(100, 0, 100, 50); + ig.setPaint(Color.yellow); + ig.fillRect(0, 50, 100, 50); + ig.setPaint(Color.red); + ig.fillRect(100, 50, 100, 50); + ig.setPaint(new Color(255, 127, 127)); + ig.fillRect(0, 100, 100, 50); + ig.setPaint(Color.black); + ig.draw(new Rectangle2D.Double(0.5, 0.5, 199, 149)); + ig.dispose(); + + image = image.getSubimage(50, 0, 50, 25); + + // Create an output stream where the PNG data + // will be stored. + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + OutputStream os = buildOutputStream(bos); + try { + // Now, try to encode image + PNGEncodeParam params = + PNGEncodeParam.getDefaultEncodeParam(image); + PNGImageEncoder pngImageEncoder = new PNGImageEncoder(os, params); + + pngImageEncoder.encode(image); + } finally { + os.close(); + } + + // Now, try to decode image + InputStream is = buildInputStream(bos); + + PNGImageDecoder pngImageDecoder + = new PNGImageDecoder(is, new PNGDecodeParam()); + + RenderedImage decodedRenderedImage = null; + decodedRenderedImage = pngImageDecoder.decodeAsRenderedImage(0); + + BufferedImage decodedImage = null; + if (decodedRenderedImage instanceof BufferedImage) { + decodedImage = (BufferedImage) decodedRenderedImage; + } else { + decodedImage = new BufferedImage(decodedRenderedImage.getWidth(), + decodedRenderedImage.getHeight(), + BufferedImage.TYPE_INT_ARGB); + ig = decodedImage.createGraphics(); + ig.drawRenderedImage(decodedRenderedImage, + new AffineTransform()); + ig.dispose(); + } + + // Compare images + if (!checkIdentical(image, decodedImage)) { + fail("Decoded image does not match the original"); + } + } + + /** + * Template method for building the PNG output stream. This gives a + * chance to sub-classes (e.g., Base64PNGEncoderTest) to add an + * additional encoding. + */ + public OutputStream buildOutputStream(ByteArrayOutputStream bos) { + return bos; + } + + /** + * Template method for building the PNG input stream. This gives a + * chance to sub-classes (e.g., Base64PNGEncoderTest) to add an + * additional decoding. + */ + public InputStream buildInputStream(ByteArrayOutputStream bos) { + return new ByteArrayInputStream(bos.toByteArray()); + } + + /** + * Compares the data for the two images + */ + public static boolean checkIdentical(BufferedImage imgA, + BufferedImage imgB) { + boolean identical = true; + if (imgA.getWidth() == imgB.getWidth() + && imgA.getHeight() == imgB.getHeight()) { + int w = imgA.getWidth(); + int h = imgA.getHeight(); + for (int i = 0; i < h; i++) { + for (int j = 0; j < w; j++) { + if (imgA.getRGB(j, i) != imgB.getRGB(j, i)) { + identical = false; + break; + } + } + if (!identical) { + break; + } + } + } + + return identical; + } + +} diff --git a/src/test/java/org/apache/xmlgraphics/image/codec/tiff/ImageInfoTestCase.java b/src/test/java/org/apache/xmlgraphics/image/codec/tiff/ImageInfoTestCase.java new file mode 100644 index 0000000..9885fac --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/image/codec/tiff/ImageInfoTestCase.java @@ -0,0 +1,188 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.xmlgraphics.image.codec.tiff; + +import java.awt.color.ColorSpace; +import java.awt.image.ColorModel; +import java.awt.image.IndexColorModel; +import java.awt.image.RenderedImage; + +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import static org.apache.xmlgraphics.image.codec.tiff.ExtraSamplesType.UNSPECIFIED; +import static org.apache.xmlgraphics.image.codec.tiff.ImageType.BILEVEL_BLACK_IS_ZERO; +import static org.apache.xmlgraphics.image.codec.tiff.ImageType.BILEVEL_WHITE_IS_ZERO; +import static org.apache.xmlgraphics.image.codec.tiff.ImageType.CIELAB; +import static org.apache.xmlgraphics.image.codec.tiff.ImageType.CMYK; +import static org.apache.xmlgraphics.image.codec.tiff.ImageType.GENERIC; +import static org.apache.xmlgraphics.image.codec.tiff.ImageType.GRAY; +import static org.apache.xmlgraphics.image.codec.tiff.ImageType.RGB; +import static org.apache.xmlgraphics.image.codec.tiff.ImageType.YCBCR; + +public class ImageInfoTestCase { + + private ColorSpace colorSpace; + private ColorModel colorModel; + private RenderedImage image; + private TIFFEncodeParam params; + + @Before + public void setUp() { + colorSpace = mock(ColorSpace.class); + colorModel = new TestColorModel(colorSpace, true); + image = mock(RenderedImage.class); + params = mock(TIFFEncodeParam.class); + } + + @Test + public void testNullColorModel() { + testImageInfo(ImageInfo.newInstance(image, 1, 1, null, params), + BILEVEL_BLACK_IS_ZERO, 0, null, 0, UNSPECIFIED); + + for (int i = 2; i < 10; i += 2) { + testImageInfo(ImageInfo.newInstance(image, 1, i, null, params), + GENERIC, i - 1, null, 0, UNSPECIFIED); + } + } + + @Test + public void testNonIndexColorModel() { + testTheColorSpaceType(ColorSpace.TYPE_CMYK, false, CMYK); + testTheColorSpaceType(ColorSpace.TYPE_GRAY, false, GRAY); + testTheColorSpaceType(ColorSpace.TYPE_RGB, true, YCBCR); + testTheColorSpaceType(ColorSpace.TYPE_RGB, false, RGB); + } + + private void testTheColorSpaceType(int colorSpaceType, boolean getJpegCompress, ImageType expectedType) { + when(colorSpace.getType()).thenReturn(colorSpaceType); + TIFFEncodeParam params = mock(TIFFEncodeParam.class); + when(params.getJPEGCompressRGBToYCbCr()).thenReturn(getJpegCompress); + + testImageInfo(ImageInfo.newInstance(image, 1, 1, colorModel, params), + expectedType, 0, null, 0, UNSPECIFIED); + } + + @Test + public void testNonIndexColorModelWithNumBandsGreaterThan1() { + testWithNumOfBandsGreaterThan1(ColorSpace.TYPE_GRAY, GRAY, 3, 1); + testWithNumOfBandsGreaterThan1(ColorSpace.TYPE_Lab, CIELAB, 6, 3); + testWithNumOfBandsGreaterThan1(ColorSpace.TYPE_CMYK, CMYK, 5, 2); + } + + private void testWithNumOfBandsGreaterThan1(int colorSpaceType, ImageType type, int numBands, + int numComponents) { + when(colorSpace.getType()).thenReturn(colorSpaceType); + when(colorSpace.getNumComponents()).thenReturn(numComponents); + testImageInfo(ImageInfo.newInstance(image, 2, numBands, colorModel, params), + type, numBands - numComponents, null, 0, UNSPECIFIED); + } + + private void testImageInfo(ImageInfo imageInfo, ImageType imageType, int numExtraSamples, + char[] colormap, int colormapSize, ExtraSamplesType extraSamplesType) { + assertEquals(imageType, imageInfo.getType()); + assertEquals(numExtraSamples, imageInfo.getNumberOfExtraSamples()); + assertArrayEquals(colormap, imageInfo.getColormap()); + assertEquals(colormapSize, imageInfo.getColormapSize()); + assertEquals(extraSamplesType, imageInfo.getExtraSamplesType()); + } + + @Test + public void testIndexColorModel() { + byte[] blackIsZero = new byte[] {0, (byte) 0xff}; + IndexColorModel icm = new IndexColorModel(1, 2, blackIsZero, blackIsZero, blackIsZero); + testImageInfo(ImageInfo.newInstance(image, 1, 1, icm, params), + BILEVEL_BLACK_IS_ZERO, 0, null, 0, UNSPECIFIED); + + byte[] whiteIsZero = new byte[] {(byte) 0xff, 0}; + icm = new IndexColorModel(1, 2, whiteIsZero, whiteIsZero, whiteIsZero); + testImageInfo(ImageInfo.newInstance(image, 1, 1, icm, params), + BILEVEL_WHITE_IS_ZERO, 0, null, 0, UNSPECIFIED); + } + + @Test + public void testTileWidthHeight() { + when(params.getWriteTiled()).thenReturn(true); + + when(image.getWidth()).thenReturn(10); + when(image.getHeight()).thenReturn(10); + + for (int i = 1; i < 10000; i += 200) { + when(params.getTileWidth()).thenReturn(i); + when(params.getTileHeight()).thenReturn(i); + int numTiles = ((10 + i - 1) / i) * ((10 + i - 1) / i); + long bytesPerRow = (long) Math.ceil((1 / 8.0) * i * 1); + long bytesPerTile = bytesPerRow * i; + + testTileOnImageInfo(ImageInfo.newInstance(image, 1, 1, colorModel, params), + i, i, numTiles, bytesPerRow, bytesPerTile); + } + } + + private void testTileOnImageInfo(ImageInfo imageInfo, int tileWidth, int tileHeight, + int numTiles, long bytesPerRow, long bytesPerTile) { + assertEquals(tileWidth, imageInfo.getTileWidth()); + assertEquals(tileHeight, imageInfo.getTileHeight()); + assertEquals(numTiles, imageInfo.getNumTiles()); + assertEquals(bytesPerRow, imageInfo.getBytesPerRow()); + assertEquals(bytesPerTile, imageInfo.getBytesPerTile()); + } + + @Test + public void testGetColormap() { + ImageInfo sut = ImageInfo.newInstance(image, 1, 1, + new IndexColorModel(1, 2, new byte[2], new byte[2], new byte[2], new byte[2]), params); + char[] colormap = sut.getColormap(); + assertEquals(0, colormap[0]); + colormap[0] = 1; + // Assert that getColormap() returns a defensive copy + assertEquals(0, sut.getColormap()[0]); + } + + private static final class TestColorModel extends ColorModel { + + protected TestColorModel(ColorSpace cspace, boolean isAlphaPremultiplied) { + super(1, new int[] {1, 1}, cspace, isAlphaPremultiplied, isAlphaPremultiplied, 1, 1); + } + + @Override + public int getRed(int pixel) { + return 0; + } + + @Override + public int getGreen(int pixel) { + return 0; + } + + @Override + public int getBlue(int pixel) { + return 0; + } + + @Override + public int getAlpha(int pixel) { + return 0; + } + } +} diff --git a/src/test/java/org/apache/xmlgraphics/image/codec/tiff/ImageTypeTestCase.java b/src/test/java/org/apache/xmlgraphics/image/codec/tiff/ImageTypeTestCase.java new file mode 100644 index 0000000..6761617 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/image/codec/tiff/ImageTypeTestCase.java @@ -0,0 +1,122 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.xmlgraphics.image.codec.tiff; + +import java.awt.color.ColorSpace; + +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import static org.apache.xmlgraphics.image.codec.tiff.ImageType.BILEVEL_BLACK_IS_ZERO; +import static org.apache.xmlgraphics.image.codec.tiff.ImageType.BILEVEL_WHITE_IS_ZERO; +import static org.apache.xmlgraphics.image.codec.tiff.ImageType.CIELAB; +import static org.apache.xmlgraphics.image.codec.tiff.ImageType.CMYK; +import static org.apache.xmlgraphics.image.codec.tiff.ImageType.GENERIC; +import static org.apache.xmlgraphics.image.codec.tiff.ImageType.GRAY; +import static org.apache.xmlgraphics.image.codec.tiff.ImageType.PALETTE; +import static org.apache.xmlgraphics.image.codec.tiff.ImageType.RGB; +import static org.apache.xmlgraphics.image.codec.tiff.ImageType.UNSUPPORTED; +import static org.apache.xmlgraphics.image.codec.tiff.ImageType.YCBCR; + +public class ImageTypeTestCase { + private static final class ColorContainer { + private final byte[] r; + private final byte[] g; + private final byte[] b; + + private ColorContainer(byte[] r, byte[] g, byte[] b) { + this.r = r; + this.b = b; + this.g = g; + } + } + + private ColorContainer blackIsZero; + private ColorContainer whiteIsZero; + + @Before + public void setUp() { + byte[] blackSetToZero = {0, (byte) 0xff}; + blackIsZero = new ColorContainer(blackSetToZero, blackSetToZero, blackSetToZero); + byte[] whiteSetToZero = {(byte) 0xff, 0}; + whiteIsZero = new ColorContainer(whiteSetToZero, whiteSetToZero, whiteSetToZero); + } + + @Test + public void testPhotometricInterpretationValue() { + assertEquals(0, BILEVEL_WHITE_IS_ZERO.getPhotometricInterpretation()); + assertEquals(1, BILEVEL_BLACK_IS_ZERO.getPhotometricInterpretation()); + assertEquals(1, GRAY.getPhotometricInterpretation()); + assertEquals(3, PALETTE.getPhotometricInterpretation()); + assertEquals(2, RGB.getPhotometricInterpretation()); + assertEquals(5, CMYK.getPhotometricInterpretation()); + assertEquals(6, YCBCR.getPhotometricInterpretation()); + assertEquals(8, CIELAB.getPhotometricInterpretation()); + assertEquals(1, GENERIC.getPhotometricInterpretation()); + } + + @Test + public void testGetTypeFromRGB() { + assertEquals(BILEVEL_BLACK_IS_ZERO, ImageType.getTypeFromRGB(2, + blackIsZero.r, blackIsZero.g, blackIsZero.b, 1, 1)); + assertEquals(BILEVEL_WHITE_IS_ZERO, ImageType.getTypeFromRGB(2, + whiteIsZero.r, whiteIsZero.g, whiteIsZero.b, 1, 1)); + // Test all other values (i.e. not including 0xff) + for (int b = 0; b < 255; b++) { + assertEquals(PALETTE, ImageType.getTypeFromRGB(2, + make2ByteArray(0, b), make2ByteArray(0, b), make2ByteArray(0, b), 1, 1)); + assertEquals(PALETTE, ImageType.getTypeFromRGB(2, + make2ByteArray(b, 0), make2ByteArray(b, 0), make2ByteArray(b, 0), 1, 1)); + if (b != 1) { + assertEquals(UNSUPPORTED, ImageType.getTypeFromRGB(2, null, null, null, 1, b)); + } + } + } + + private byte[] make2ByteArray(int b1, int b2) { + return new byte[] {(byte) b1, (byte) b2}; + } + + @Test(expected = IllegalArgumentException.class) + public void testException() { + assertEquals(UNSUPPORTED, ImageType.getTypeFromRGB(1, null, null, null, 1, 1)); + } + + @Test + public void testGetTypeFromColorSpace() { + testIndividualColorSpaceType(CMYK, ColorSpace.TYPE_CMYK, false); + testIndividualColorSpaceType(GRAY, ColorSpace.TYPE_GRAY, false); + testIndividualColorSpaceType(CIELAB, ColorSpace.TYPE_Lab, false); + testIndividualColorSpaceType(YCBCR, ColorSpace.TYPE_YCbCr, false); + testIndividualColorSpaceType(YCBCR, ColorSpace.TYPE_RGB, true); + testIndividualColorSpaceType(RGB, ColorSpace.TYPE_RGB, false); + } + + private void testIndividualColorSpaceType(ImageType expected, int type, boolean getJpegCompress) { + ColorSpace colorSpace = mock(ColorSpace.class); + when(colorSpace.getType()).thenReturn(type); + TIFFEncodeParam params = mock(TIFFEncodeParam.class); + when(params.getJPEGCompressRGBToYCbCr()).thenReturn(getJpegCompress); + + assertEquals(expected, ImageType.getTypeFromColorSpace(colorSpace, params)); + } +} diff --git a/src/test/java/org/apache/xmlgraphics/image/codec/tiff/TIFFImageEncoderTestCase.java b/src/test/java/org/apache/xmlgraphics/image/codec/tiff/TIFFImageEncoderTestCase.java new file mode 100644 index 0000000..1603679 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/image/codec/tiff/TIFFImageEncoderTestCase.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.xmlgraphics.image.codec.tiff; + +import java.awt.Color; +import java.awt.Graphics; +import java.awt.image.BufferedImage; +import java.awt.image.RenderedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import org.junit.Test; + +import static org.junit.Assert.assertArrayEquals; + +import org.apache.commons.io.IOUtils; + +public class TIFFImageEncoderTestCase { + + @Test + public void testGrayImage() throws IOException { + testImage(BufferedImage.TYPE_BYTE_GRAY, "gray.tiff"); + } + + @Test + public void testBilevelImage() throws IOException { + testImage(BufferedImage.TYPE_BYTE_BINARY, "bilevel.tiff"); + } + + private void testImage(int imageType, String imageFileName) throws IOException { + ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); + TIFFImageEncoder encoder = new TIFFImageEncoder(byteStream, null); + encoder.encode(getImage(imageType)); + byte[] actualArray = IOUtils.toByteArray(getClass().getResource(imageFileName).openStream()); + assertArrayEquals(byteStream.toByteArray(), actualArray); + } + + private RenderedImage getImage(int imageType) { + BufferedImage img = new BufferedImage(400, 400, imageType); + img.getSampleModel(); + Graphics gfx = img.getGraphics(); + gfx.setColor(Color.RED); + gfx.fillRect(10, 10, 100, 100); + gfx.setColor(Color.WHITE); + gfx.fillRect(50, 50, 100, 100); + return img; + } +} diff --git a/src/test/java/org/apache/xmlgraphics/image/codec/tiff/bilevel.tiff b/src/test/java/org/apache/xmlgraphics/image/codec/tiff/bilevel.tiff new file mode 100644 index 0000000..23ec9da Binary files /dev/null and b/src/test/java/org/apache/xmlgraphics/image/codec/tiff/bilevel.tiff differ diff --git a/src/test/java/org/apache/xmlgraphics/image/codec/tiff/gray.tiff b/src/test/java/org/apache/xmlgraphics/image/codec/tiff/gray.tiff new file mode 100644 index 0000000..3f66bd5 Binary files /dev/null and b/src/test/java/org/apache/xmlgraphics/image/codec/tiff/gray.tiff differ diff --git a/src/test/java/org/apache/xmlgraphics/image/loader/CorruptImagesTestCase.java b/src/test/java/org/apache/xmlgraphics/image/loader/CorruptImagesTestCase.java new file mode 100644 index 0000000..f678cf2 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/image/loader/CorruptImagesTestCase.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: CorruptImagesTestCase.java 1830541 2018-04-30 09:30:55Z ssteiner $ */ + +package org.apache.xmlgraphics.image.loader; + +import org.junit.Test; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +/** + * Tests for error behaviour with corrupt images. + */ +public class CorruptImagesTestCase { + + private MockImageContext imageContext = MockImageContext.getInstance(); + + @Test + public void testCorruptPNG() throws Exception { + String uri = "corrupt-image.png"; + + ImageSessionContext sessionContext = imageContext.newSessionContext(); + ImageManager manager = imageContext.getImageManager(); + + try { + manager.preloadImage(uri, sessionContext); + fail("Expected an ImageException!"); + } catch (Exception ie) { + //Expected exception + assertNotNull(ie.getMessage()); + } + } + +} diff --git a/src/test/java/org/apache/xmlgraphics/image/loader/DemoPainter.java b/src/test/java/org/apache/xmlgraphics/image/loader/DemoPainter.java new file mode 100644 index 0000000..c55bf69 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/image/loader/DemoPainter.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DemoPainter.java 606580 2007-12-23 17:45:02Z jeremias $ */ + +package org.apache.xmlgraphics.image.loader; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.geom.Rectangle2D; + +import org.apache.xmlgraphics.java2d.Graphics2DImagePainter; + +public class DemoPainter implements Graphics2DImagePainter { + + /** {@inheritDoc} */ + public Dimension getImageSize() { + return new Dimension(10000, 10000); + } + + public void paint(Graphics2D g2d, Rectangle2D area) { + g2d.translate(area.getX(), area.getY()); + double w = area.getWidth(); + double h = area.getHeight(); + + //Fit in paint area + Dimension imageSize = getImageSize(); + double sx = w / imageSize.getWidth(); + double sy = h / imageSize.getHeight(); + if (sx != 1.0 || sy != 1.0) { + g2d.scale(sx, sy); + } + + g2d.setColor(Color.BLACK); + g2d.setStroke(new BasicStroke()); + g2d.drawRect(0, 0, imageSize.width, imageSize.height); + g2d.drawOval(0, 0, imageSize.width, imageSize.height); + } + +} diff --git a/src/test/java/org/apache/xmlgraphics/image/loader/ImageFlavorTestCase.java b/src/test/java/org/apache/xmlgraphics/image/loader/ImageFlavorTestCase.java new file mode 100644 index 0000000..6aa0c1f --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/image/loader/ImageFlavorTestCase.java @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageFlavorTestCase.java 1365650 2012-07-25 15:59:30Z mehdi $ */ + +package org.apache.xmlgraphics.image.loader; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import org.apache.xmlgraphics.util.MimeConstants; + +/** + * Tests for image flavors. + */ +public class ImageFlavorTestCase { + + @Test + public void testBasicFlavors() throws Exception { + ImageFlavor f1; + ImageFlavor f2; + + f1 = ImageFlavor.RAW_JPEG; + f2 = ImageFlavor.RAW_PNG; + assertFalse(f1.equals(f2)); + assertEquals(MimeConstants.MIME_JPEG, f1.getMimeType()); + assertNull(f1.getNamespace()); + assertEquals(MimeConstants.MIME_PNG, f2.getMimeType()); + assertNull(f2.getNamespace()); + + f1 = ImageFlavor.GRAPHICS2D; + f2 = new ImageFlavor(ImageFlavor.GRAPHICS2D.getName()); + assertTrue(f1.equals(f2)); + assertNull(f1.getMimeType()); + assertNull(f1.getNamespace()); + } + + @Test + public void testRefinedFlavors() throws Exception { + ImageFlavor f1; + ImageFlavor f2; + + f1 = ImageFlavor.RENDERED_IMAGE; + f2 = ImageFlavor.BUFFERED_IMAGE; + assertFalse(f1.equals(f2)); + assertTrue(f2.isCompatible(f1)); + assertFalse(f1.isCompatible(f2)); + + assertNull(f1.getMimeType()); + assertNull(f1.getNamespace()); + assertNull(f2.getMimeType()); + assertNull(f2.getNamespace()); + + f1 = ImageFlavor.XML_DOM; + f2 = new XMLNamespaceEnabledImageFlavor(ImageFlavor.XML_DOM, "http://www.w3.org/2000/svg"); + assertFalse(f1.equals(f2)); + assertTrue(f2.isCompatible(f1)); + assertFalse(f1.isCompatible(f2)); + + assertEquals("text/xml", f1.getMimeType()); + assertNull(f1.getNamespace()); + assertEquals("text/xml", f2.getMimeType()); + assertEquals("http://www.w3.org/2000/svg", f2.getNamespace()); + } + +} diff --git a/src/test/java/org/apache/xmlgraphics/image/loader/ImageLoaderTestCase.java b/src/test/java/org/apache/xmlgraphics/image/loader/ImageLoaderTestCase.java new file mode 100644 index 0000000..b1acf39 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/image/loader/ImageLoaderTestCase.java @@ -0,0 +1,300 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageLoaderTestCase.java 1365650 2012-07-25 15:59:30Z mehdi $ */ + +package org.apache.xmlgraphics.image.loader; + +import java.awt.color.ICC_Profile; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import javax.imageio.stream.ImageInputStream; +import javax.xml.transform.Source; +import javax.xml.transform.stream.StreamSource; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +import org.apache.commons.io.IOUtils; + +import org.apache.xmlgraphics.image.loader.impl.ImageLoaderPNG; +import org.apache.xmlgraphics.image.loader.impl.ImageLoaderRawPNG; +import org.apache.xmlgraphics.image.loader.impl.ImageRawStream; +import org.apache.xmlgraphics.image.loader.impl.ImageRendered; +import org.apache.xmlgraphics.image.loader.spi.ImageImplRegistry; +import org.apache.xmlgraphics.image.loader.spi.ImageLoader; +import org.apache.xmlgraphics.image.loader.spi.ImageLoaderFactory; + +/** + * Tests for bundled ImageLoader implementations. + */ +public class ImageLoaderTestCase { + + private MockImageContext imageContext = MockImageContext.getInstance(); + + private MyImageSessionContext createImageSessionContext() { + return new MyImageSessionContext(imageContext); + } + + @Test + public void testPNG() throws Exception { + String uri = "asf-logo.png"; + + MyImageSessionContext sessionContext = createImageSessionContext(); + ImageManager manager = imageContext.getImageManager(); + + ImageInfo info = manager.preloadImage(uri, sessionContext); + assertNotNull("ImageInfo must not be null", info); + + Image img = manager.getImage(info, ImageFlavor.RENDERED_IMAGE, sessionContext); + assertNotNull("Image must not be null", img); + assertEquals(ImageFlavor.RENDERED_IMAGE, img.getFlavor()); + ImageRendered imgRed = (ImageRendered) img; + assertNotNull(imgRed.getRenderedImage()); + assertEquals(169, imgRed.getRenderedImage().getWidth()); + assertEquals(51, imgRed.getRenderedImage().getHeight()); + info = imgRed.getInfo(); //Switch to the ImageInfo returned by the image + assertEquals(126734, info.getSize().getWidthMpt()); + assertEquals(38245, info.getSize().getHeightMpt()); + + sessionContext.checkAllStreamsClosed(); + } + + @Test + public void testGIF() throws Exception { + String uri = "bgimg72dpi.gif"; + + MyImageSessionContext sessionContext = createImageSessionContext(); + ImageManager manager = imageContext.getImageManager(); + + ImageInfo info = manager.preloadImage(uri, sessionContext); + assertNotNull("ImageInfo must not be null", info); + + Image img = manager.getImage(info, ImageFlavor.RENDERED_IMAGE, sessionContext); + assertNotNull("Image must not be null", img); + assertEquals(ImageFlavor.RENDERED_IMAGE, img.getFlavor()); + ImageRendered imgRed = (ImageRendered) img; + assertNotNull(imgRed.getRenderedImage()); + assertEquals(192, imgRed.getRenderedImage().getWidth()); + assertEquals(192, imgRed.getRenderedImage().getHeight()); + info = imgRed.getInfo(); //Switch to the ImageInfo returned by the image + assertEquals(192000, info.getSize().getWidthMpt()); + assertEquals(192000, info.getSize().getHeightMpt()); + + sessionContext.checkAllStreamsClosed(); + } + + @Test + public void testEPSASCII() throws Exception { + String uri = "barcode.eps"; + + MyImageSessionContext sessionContext = createImageSessionContext(); + ImageManager manager = imageContext.getImageManager(); + + ImageInfo info = manager.preloadImage(uri, sessionContext); + assertNotNull("ImageInfo must not be null", info); + + Image img = manager.getImage(info, ImageFlavor.RAW_EPS, sessionContext); + assertNotNull("Image must not be null", img); + assertEquals(ImageFlavor.RAW_EPS, img.getFlavor()); + ImageRawStream imgEPS = (ImageRawStream) img; + InputStream in = imgEPS.createInputStream(); + try { + assertNotNull(in); + Reader reader = new InputStreamReader(in, "US-ASCII"); + char[] c = new char[4]; + reader.read(c); + if (!("%!PS".equals(new String(c)))) { + fail("EPS header expected"); + } + } finally { + IOUtils.closeQuietly(in); + } + + sessionContext.checkAllStreamsClosed(); + } + + @Test + public void testEPSBinary() throws Exception { + String uri = "img-with-tiff-preview.eps"; + + MyImageSessionContext sessionContext = createImageSessionContext(); + ImageManager manager = imageContext.getImageManager(); + + ImageInfo info = manager.preloadImage(uri, sessionContext); + assertNotNull("ImageInfo must not be null", info); + + Image img = manager.getImage(info, ImageFlavor.RAW_EPS, sessionContext); + assertNotNull("Image must not be null", img); + assertEquals(ImageFlavor.RAW_EPS, img.getFlavor()); + ImageRawStream imgEPS = (ImageRawStream) img; + InputStream in = imgEPS.createInputStream(); + try { + assertNotNull(in); + Reader reader = new InputStreamReader(in, "US-ASCII"); + char[] c = new char[4]; + reader.read(c); + if (!("%!PS".equals(new String(c)))) { + fail("EPS header expected"); + } + } finally { + IOUtils.closeQuietly(in); + } + + sessionContext.checkAllStreamsClosed(); + } + + @Test + public void testICCProfiles() throws Exception { + MyImageSessionContext sessionContext = createImageSessionContext(); + List profiles = new ArrayList(); + + runReaders(profiles, sessionContext, "iccTest.png", "image/png", + ImageFlavor.RAW_PNG); + runReaders(profiles, sessionContext, "iccTest.jpg", "image/jpeg", + ImageFlavor.RAW_JPEG); + + ICC_Profile first = profiles.get(0); + byte[] firstData = first.getData(); + for (int i = 1; i < profiles.size(); i++) { + ICC_Profile icc = profiles.get(i); + byte[] data = icc.getData(); + assertEquals("Embedded ICC Profiles are not the same size!", + firstData.length, data.length); + for (int j = 0; j < firstData.length; j++) { + assertEquals("Embedded ICC Profiles differ at index " + j, + firstData[j], data[j]); + } + } + } + + private void runReaders(List profiles, ImageSessionContext isc, String uri, + String mime, ImageFlavor rawFlavor) throws Exception { + ImageLoaderFactory[] ilfs = ImageImplRegistry.getDefaultInstance() + .getImageLoaderFactories(mime); + if (ilfs != null) { + for (int i = 0; i < ilfs.length; i++) { + ImageLoaderFactory ilf = ilfs[i]; + try { + final ImageLoader il = ilf.newImageLoader(rawFlavor); + if (il instanceof ImageLoaderRawPNG || il instanceof ImageLoaderPNG) { + // temporary measure until ImageLoaderRawPNG and ImageLoader PNG handle ICC profiles + continue; + } + final ImageInfo im = new ImageInfo(uri, mime); + final Image img = il.loadImage(im, isc); + final ICC_Profile icc = img.getICCProfile(); + // Assume the profile can only be correct if the image could + // actually be interpreted. + if (img.getColorSpace() != null) { + profiles.add(icc); + } + } catch (IllegalArgumentException e) { + // Ignore. This imageLoader does not support RAW + } + try { + final ImageLoader il = ilf.newImageLoader(ImageFlavor.BUFFERED_IMAGE); + final ImageInfo im = new ImageInfo(uri, mime); + final Image img = il.loadImage(im, isc); + final ICC_Profile icc = img.getICCProfile(); + profiles.add(icc); + } catch (IllegalArgumentException e) { + // Ignore. This imageLoader does not support Buffered. + } + } + } + } + + @Test + public void testBrokenIccPng() throws Exception { + String uri = "corrupt-icc.png"; + + MyImageSessionContext sessionContext = createImageSessionContext(); + ImageManager manager = imageContext.getImageManager(); + + ImageInfo info = manager.preloadImage(uri, sessionContext); + assertNotNull("ImageInfo must not be null", info); + + Image img = manager.getImage(info, ImageFlavor.RENDERED_IMAGE, sessionContext); + assertNotNull("Image must not be null", img); + assertEquals(ImageFlavor.RENDERED_IMAGE, img.getFlavor()); + ImageRendered imgRed = (ImageRendered) img; + assertNotNull(imgRed.getRenderedImage()); + assertEquals(400, imgRed.getRenderedImage().getWidth()); + assertEquals(300, imgRed.getRenderedImage().getHeight()); + + sessionContext.checkAllStreamsClosed(); + } + + private static class MyImageSessionContext extends MockImageSessionContext { + + private List streams = new java.util.ArrayList(); + + public MyImageSessionContext(ImageContext context) { + super(context); + } + + public Source newSource(String uri) { + Source src = super.newSource(uri); + if (src instanceof ImageSource) { + ImageSource is = (ImageSource) src; + ImageInputStream in = is.getImageInputStream(); + //in = new ObservableImageInputStream(in, is.getSystemId()); + in = ObservableStream.Factory.observe(in, is.getSystemId()); + streams.add(in); + is.setImageInputStream(in); + } + return src; + } + + /** {@inheritDoc} */ + protected Source resolveURI(String uri) { + Source src = super.resolveURI(uri); + if (src instanceof StreamSource) { + StreamSource ss = (StreamSource) src; + if (ss.getInputStream() != null) { + InputStream in = new ObservableInputStream( + ss.getInputStream(), ss.getSystemId()); + streams.add(in); + ss.setInputStream(in); + } + } + return src; + } + + public void checkAllStreamsClosed() { + Iterator iter = streams.iterator(); + while (iter.hasNext()) { + ObservableStream stream = (ObservableStream) iter.next(); + iter.remove(); + if (!stream.isClosed()) { + fail(stream.getClass().getName() + " is NOT closed: " + stream.getSystemID()); + } + } + } + + } + +} diff --git a/src/test/java/org/apache/xmlgraphics/image/loader/ImagePipelineTestCase.java b/src/test/java/org/apache/xmlgraphics/image/loader/ImagePipelineTestCase.java new file mode 100644 index 0000000..d2c9892 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/image/loader/ImagePipelineTestCase.java @@ -0,0 +1,149 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImagePipelineTestCase.java 1365650 2012-07-25 15:59:30Z mehdi $ */ + +package org.apache.xmlgraphics.image.loader; + +import java.awt.Dimension; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import org.apache.xmlgraphics.image.loader.impl.ImageConverterBuffered2Rendered; +import org.apache.xmlgraphics.image.loader.impl.ImageConverterG2D2Bitmap; +import org.apache.xmlgraphics.image.loader.impl.ImageConverterRendered2PNG; +import org.apache.xmlgraphics.image.loader.impl.ImageGraphics2D; +import org.apache.xmlgraphics.image.loader.impl.ImageRawStream; +import org.apache.xmlgraphics.image.loader.impl.imageio.ImageLoaderImageIO; +import org.apache.xmlgraphics.image.loader.pipeline.ImageProviderPipeline; +import org.apache.xmlgraphics.image.loader.spi.ImageLoader; +import org.apache.xmlgraphics.java2d.Graphics2DImagePainter; +import org.apache.xmlgraphics.util.MimeConstants; + +/** + * Tests for the image pipeline functionality. + */ +public class ImagePipelineTestCase { + + private MockImageContext imageContext = MockImageContext.getInstance(); + + @Test + public void testPipelineWithLoader() throws Exception { + String uri = "bgimg72dpi.gif"; + + ImageSessionContext sessionContext = imageContext.newSessionContext(); + ImageManager manager = imageContext.getImageManager(); + + ImageInfo info = manager.preloadImage(uri, sessionContext); + assertNotNull("ImageInfo must not be null", info); + + ImageLoader loader = new ImageLoaderImageIO(ImageFlavor.RENDERED_IMAGE); + ImageProviderPipeline pipeline = new ImageProviderPipeline(manager.getCache(), loader); + pipeline.addConverter(new ImageConverterRendered2PNG()); + + Image img = pipeline.execute(info, null, sessionContext); + assertNotNull("Image must not be null", img); + assertEquals(ImageFlavor.RAW_PNG, img.getFlavor()); + assertTrue(img instanceof ImageRawStream); + + //Original MIME type stays the same, but the flavor MIME changes + assertEquals(MimeConstants.MIME_GIF, img.getInfo().getMimeType()); + assertEquals(MimeConstants.MIME_PNG, img.getFlavor().getMimeType()); + } + + @Test + public void testPipelineWithoutLoader() throws Exception { + + ImageSessionContext sessionContext = imageContext.newSessionContext(); + ImageManager manager = imageContext.getImageManager(); + + Image original = createG2DImage(); + + ImageProviderPipeline pipeline = new ImageProviderPipeline(manager.getCache(), null); + pipeline.addConverter(new ImageConverterG2D2Bitmap()); + pipeline.addConverter(new ImageConverterBuffered2Rendered()); + pipeline.addConverter(new ImageConverterRendered2PNG()); + + Image img = pipeline.execute(original.getInfo(), original, null, + sessionContext); + assertNotNull("Image must not be null", img); + assertEquals(ImageFlavor.RAW_PNG, img.getFlavor()); + assertTrue(img instanceof ImageRawStream); + + //Original MIME type stays the same, but the flavor MIME changes + assertNull(img.getInfo().getMimeType()); + assertEquals(MimeConstants.MIME_PNG, img.getFlavor().getMimeType()); + + } + + private Image createG2DImage() { + Graphics2DImagePainter painter = new DemoPainter(); + Dimension dim = painter.getImageSize(); + + ImageSize size = new ImageSize(); + size.setSizeInMillipoints(dim.width, dim.height); + size.setResolution(72); + size.calcPixelsFromSize(); + + ImageInfo info = new ImageInfo(null /*null is the intention here*/, null); + info.setSize(size); + ImageGraphics2D g2dImage = new ImageGraphics2D(info, painter); + return g2dImage; + } + + @Test + public void testPipelineFromURIThroughManager() throws Exception { + String uri = "asf-logo.png"; + + ImageSessionContext sessionContext = imageContext.newSessionContext(); + ImageManager manager = imageContext.getImageManager(); + + ImageInfo info = manager.preloadImage(uri, sessionContext); + assertNotNull("ImageInfo must not be null", info); + + ImageFlavor[] flavors = new ImageFlavor[] { + ImageFlavor.RAW_PNG, ImageFlavor.RAW_JPEG + }; + Image img = manager.getImage(info, flavors, sessionContext); + + assertNotNull("Image must not be null", img); + assertEquals(ImageFlavor.RAW_PNG, img.getFlavor()); + assertTrue(img instanceof ImageRawStream); + } + + @Test + public void testPipelineWithoutURIThroughManager() throws Exception { + ImageManager manager = imageContext.getImageManager(); + + Image original = createG2DImage(); + + ImageFlavor[] flavors = new ImageFlavor[] { + ImageFlavor.RAW_PNG, ImageFlavor.RAW_JPEG + }; + Image img = manager.convertImage(original, flavors); + + assertNotNull("Image must not be null", img); + assertEquals(ImageFlavor.RAW_PNG, img.getFlavor()); + assertTrue(img instanceof ImageRawStream); + } + +} diff --git a/src/test/java/org/apache/xmlgraphics/image/loader/ImagePreloaderTestCase.java b/src/test/java/org/apache/xmlgraphics/image/loader/ImagePreloaderTestCase.java new file mode 100644 index 0000000..50bb19b --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/image/loader/ImagePreloaderTestCase.java @@ -0,0 +1,357 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImagePreloaderTestCase.java 1365650 2012-07-25 15:59:30Z mehdi $ */ + +package org.apache.xmlgraphics.image.loader; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; + +import javax.xml.transform.Source; +import javax.xml.transform.TransformerException; +import javax.xml.transform.URIResolver; +import javax.xml.transform.sax.SAXSource; + +import org.junit.Test; +import org.xml.sax.InputSource; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +import org.apache.xmlgraphics.image.loader.spi.ImageLoaderFactory; +import org.apache.xmlgraphics.util.MimeConstants; + +/** + * Tests for bundled Imagepreloader implementations. + */ +public class ImagePreloaderTestCase { + + private MockImageContext imageContext = MockImageContext.getInstance(); + + @Test + public void testImageLoaderFactory() throws Exception { + ImageManager manager = imageContext.getImageManager(); + ImageInfo info = new ImageInfo(null, MimeConstants.MIME_PNG); + ImageLoaderFactory ilf = manager.getRegistry().getImageLoaderFactory( + info, ImageFlavor.BUFFERED_IMAGE); + assertNotNull(ilf); + } + + @Test + public void testFileNotFound() throws Exception { + String uri = "doesnotexistanywhere.png"; + + ImageSessionContext sessionContext = imageContext.newSessionContext(); + ImageManager manager = imageContext.getImageManager(); + try { + manager.preloadImage(uri, sessionContext); + fail("Expected a FileNotFoundException!"); + } catch (FileNotFoundException e) { + //expected! + } + } + + @Test + public void testPNG() throws Exception { + String uri = "asf-logo.png"; + + ImageSessionContext sessionContext = imageContext.newSessionContext(); + ImageManager manager = imageContext.getImageManager(); + + ImageInfo info = manager.preloadImage(uri, sessionContext); + assertNotNull("ImageInfo must not be null", info); + assertEquals(MimeConstants.MIME_PNG, info.getMimeType()); + assertEquals("asf-logo.png", info.getOriginalURI()); + assertEquals(169, info.getSize().getWidthPx()); + assertEquals(51, info.getSize().getHeightPx()); + assertEquals(96, info.getSize().getDpiHorizontal(), 0.1); + assertEquals(126734, info.getSize().getWidthMpt()); + assertEquals(38245, info.getSize().getHeightMpt()); + } + + @Test + public void testPNGNoResolution() throws Exception { + String uri = "no-resolution.png"; + //This file contains a pHYs chunk but the resolution is set to zero. + //Reported in Bugzilla #45789 + + ImageSessionContext sessionContext = imageContext.newSessionContext(); + ImageManager manager = imageContext.getImageManager(); + + ImageInfo info = manager.preloadImage(uri, sessionContext); + assertNotNull("ImageInfo must not be null", info); + assertEquals(MimeConstants.MIME_PNG, info.getMimeType()); + assertEquals("no-resolution.png", info.getOriginalURI()); + assertEquals(51, info.getSize().getWidthPx()); + assertEquals(24, info.getSize().getHeightPx()); + //Without resolution information (or resolution=0), the default shall be used + assertEquals(72, info.getSize().getDpiHorizontal(), 0.1); + assertEquals(51000, info.getSize().getWidthMpt()); + assertEquals(24000, info.getSize().getHeightMpt()); + } + + @Test + public void testTIFF() throws Exception { + String uri = "tiff_group4.tif"; + + ImageSessionContext sessionContext = imageContext.newSessionContext(); + ImageManager manager = imageContext.getImageManager(); + + ImageInfo info = manager.preloadImage(uri, sessionContext); + assertNotNull("ImageInfo must not be null", info); + assertEquals(MimeConstants.MIME_TIFF, info.getMimeType()); + assertEquals(uri, info.getOriginalURI()); + assertEquals(1560, info.getSize().getWidthPx()); + assertEquals(189, info.getSize().getHeightPx()); + assertEquals(204, info.getSize().getDpiHorizontal(), 0.1); + assertEquals(550588, info.getSize().getWidthMpt()); + assertEquals(66706, info.getSize().getHeightMpt()); + } + + @Test + public void testTIFFNoResolution() throws Exception { + String uri = "no-resolution.tif"; + + ImageSessionContext sessionContext = imageContext.newSessionContext(); + ImageManager manager = imageContext.getImageManager(); + + ImageInfo info = manager.preloadImage(uri, sessionContext); + assertNotNull("ImageInfo must not be null", info); + assertEquals(MimeConstants.MIME_TIFF, info.getMimeType()); + assertEquals(uri, info.getOriginalURI()); + assertEquals(51, info.getSize().getWidthPx()); + assertEquals(24, info.getSize().getHeightPx()); + assertEquals(imageContext.getSourceResolution(), info.getSize().getDpiHorizontal(), 0.1); + assertEquals(51000, info.getSize().getWidthMpt()); + assertEquals(24000, info.getSize().getHeightMpt()); + } + + @Test + public void testGIF() throws Exception { + String uri = "bgimg72dpi.gif"; + + ImageSessionContext sessionContext = imageContext.newSessionContext(); + ImageManager manager = imageContext.getImageManager(); + + ImageInfo info = manager.preloadImage(uri, sessionContext); + assertNotNull("ImageInfo must not be null", info); + assertEquals(MimeConstants.MIME_GIF, info.getMimeType()); + assertEquals(uri, info.getOriginalURI()); + assertEquals(192, info.getSize().getWidthPx()); + assertEquals(192, info.getSize().getHeightPx()); + assertEquals(imageContext.getSourceResolution(), info.getSize().getDpiHorizontal(), 0.1); + assertEquals(192000, info.getSize().getWidthMpt()); + assertEquals(192000, info.getSize().getHeightMpt()); + } + + @Test + public void testEMF() throws Exception { + String uri = "img.emf"; + + ImageSessionContext sessionContext = imageContext.newSessionContext(); + ImageManager manager = imageContext.getImageManager(); + + ImageInfo info = manager.preloadImage(uri, sessionContext); + assertNotNull("ImageInfo must not be null", info); + assertEquals("image/emf", info.getMimeType()); + assertEquals(uri, info.getOriginalURI()); + assertEquals(76, info.getSize().getWidthPx()); + assertEquals(76, info.getSize().getHeightPx()); + assertEquals(96, info.getSize().getDpiHorizontal(), 1.0); + assertEquals(56665, info.getSize().getWidthMpt()); + assertEquals(56665, info.getSize().getHeightMpt()); + } + + @Test + public void testJPEG1() throws Exception { + String uri = "bgimg300dpi.jpg"; + + ImageSessionContext sessionContext = imageContext.newSessionContext(); + ImageManager manager = imageContext.getImageManager(); + + ImageInfo info = manager.preloadImage(uri, sessionContext); + assertNotNull("ImageInfo must not be null", info); + assertEquals(MimeConstants.MIME_JPEG, info.getMimeType()); + assertEquals(uri, info.getOriginalURI()); + assertEquals(192, info.getSize().getWidthPx()); + assertEquals(192, info.getSize().getHeightPx()); + assertEquals(300, info.getSize().getDpiHorizontal(), 0.1); + assertEquals(46080, info.getSize().getWidthMpt()); + assertEquals(46080, info.getSize().getHeightMpt()); + } + + @Test + public void testJPEG2() throws Exception { + String uri = "cmyk.jpg"; + + ImageSessionContext sessionContext = imageContext.newSessionContext(); + ImageManager manager = imageContext.getImageManager(); + + ImageInfo info = manager.preloadImage(uri, sessionContext); + assertNotNull("ImageInfo must not be null", info); + assertEquals(MimeConstants.MIME_JPEG, info.getMimeType()); + assertEquals(uri, info.getOriginalURI()); + assertEquals(160, info.getSize().getWidthPx()); + assertEquals(35, info.getSize().getHeightPx()); + assertEquals(72, info.getSize().getDpiHorizontal(), 0.1); + assertEquals(160000, info.getSize().getWidthMpt()); + assertEquals(35000, info.getSize().getHeightMpt()); + } + + @Test + public void testJPEG3() throws Exception { + String uri = "cmyk-pxcm.jpg"; //Contains resolution as pixels per centimeter + + ImageSessionContext sessionContext = imageContext.newSessionContext(); + ImageManager manager = imageContext.getImageManager(); + + ImageInfo info = manager.preloadImage(uri, sessionContext); + assertNotNull("ImageInfo must not be null", info); + assertEquals(MimeConstants.MIME_JPEG, info.getMimeType()); + assertEquals(uri, info.getOriginalURI()); + assertEquals(160, info.getSize().getWidthPx()); + assertEquals(35, info.getSize().getHeightPx()); + assertEquals(71.1, info.getSize().getDpiHorizontal(), 0.1); //28 px/cm = 71.1199 dpi + assertEquals(161980, info.getSize().getWidthMpt()); + assertEquals(35433, info.getSize().getHeightMpt()); + } + + @Test + public void testBMP() throws Exception { + String uri = "bgimg300dpi.bmp"; + + ImageSessionContext sessionContext = imageContext.newSessionContext(); + ImageManager manager = imageContext.getImageManager(); + + ImageInfo info = manager.preloadImage(uri, sessionContext); + assertNotNull("ImageInfo must not be null", info); + assertEquals("image/bmp", info.getMimeType()); + assertEquals(uri, info.getOriginalURI()); + assertEquals(192, info.getSize().getWidthPx()); + assertEquals(192, info.getSize().getHeightPx()); + assertEquals(300, info.getSize().getDpiHorizontal(), 0.1); + assertEquals(46092, info.getSize().getWidthMpt()); + assertEquals(46092, info.getSize().getHeightMpt()); + } + + @Test + public void testBMPNoResolution() throws Exception { + String uri = "no-resolution.bmp"; + + ImageSessionContext sessionContext = imageContext.newSessionContext(); + ImageManager manager = imageContext.getImageManager(); + + ImageInfo info = manager.preloadImage(uri, sessionContext); + assertNotNull("ImageInfo must not be null", info); + assertEquals("image/bmp", info.getMimeType()); + assertEquals(uri, info.getOriginalURI()); + assertEquals(50, info.getSize().getWidthPx()); + assertEquals(50, info.getSize().getHeightPx()); + assertEquals(imageContext.getSourceResolution(), info.getSize().getDpiHorizontal(), 0.1); + assertEquals(50000, info.getSize().getWidthMpt()); + assertEquals(50000, info.getSize().getHeightMpt()); + } + + @Test + public void testEPSAscii() throws Exception { + String uri = "barcode.eps"; + + ImageSessionContext sessionContext = imageContext.newSessionContext(); + ImageManager manager = imageContext.getImageManager(); + + ImageInfo info = manager.preloadImage(uri, sessionContext); + assertNotNull("ImageInfo must not be null", info); + assertEquals(MimeConstants.MIME_EPS, info.getMimeType()); + assertEquals(uri, info.getOriginalURI()); + assertEquals(136, info.getSize().getWidthPx()); + assertEquals(43, info.getSize().getHeightPx()); + assertEquals(imageContext.getSourceResolution(), info.getSize().getDpiHorizontal(), 0.1); + assertEquals(135655, info.getSize().getWidthMpt()); + assertEquals(42525, info.getSize().getHeightMpt()); + } + + @Test + public void testEPSBinary() throws Exception { + String uri = "img-with-tiff-preview.eps"; + + ImageSessionContext sessionContext = imageContext.newSessionContext(); + ImageManager manager = imageContext.getImageManager(); + + ImageInfo info = manager.preloadImage(uri, sessionContext); + assertNotNull("ImageInfo must not be null", info); + assertEquals(MimeConstants.MIME_EPS, info.getMimeType()); + assertEquals(uri, info.getOriginalURI()); + assertEquals(17, info.getSize().getWidthPx()); + assertEquals(17, info.getSize().getHeightPx()); + assertEquals(imageContext.getSourceResolution(), info.getSize().getDpiHorizontal(), 0.1); + assertEquals(17000, info.getSize().getWidthMpt()); + assertEquals(17000, info.getSize().getHeightMpt()); + } + + @Test + public void testSAXSourceWithSystemID() throws Exception { + URIResolver resolver = new URIResolver() { + public Source resolve(String href, String base) throws TransformerException { + if (href.startsWith("img:")) { + String filename = href.substring(4); + InputSource is = new InputSource(base + filename); + return new SAXSource(is); + } else { + return null; + } + } + }; + checkImageFound("img:asf-logo.png", resolver); + } + + @Test + public void testSAXSourceWithInputStream() throws Exception { + URIResolver resolver = new URIResolver() { + public Source resolve(String href, String base) throws TransformerException { + if (href.startsWith("img:")) { + String filename = href.substring(4); + InputSource is; + try { + is = new InputSource(new java.io.FileInputStream( + new File(MockImageSessionContext.IMAGE_BASE_DIR, filename))); + } catch (FileNotFoundException e) { + throw new TransformerException(e); + } + return new SAXSource(is); + } else { + return null; + } + } + }; + checkImageFound("img:asf-logo.png", resolver); + } + + private void checkImageFound(String uri, URIResolver resolver) + throws ImageException, IOException { + ImageSessionContext sessionContext = new SimpleURIResolverBasedImageSessionContext( + imageContext, MockImageSessionContext.IMAGE_BASE_DIR, resolver); + ImageManager manager = imageContext.getImageManager(); + + ImageInfo info = manager.preloadImage(uri, sessionContext); + assertNotNull("ImageInfo must not be null", info); + assertEquals(uri, info.getOriginalURI()); + } + +} diff --git a/src/test/java/org/apache/xmlgraphics/image/loader/ImageSessionContextTestCase.java b/src/test/java/org/apache/xmlgraphics/image/loader/ImageSessionContextTestCase.java new file mode 100644 index 0000000..5f4f5da --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/image/loader/ImageSessionContextTestCase.java @@ -0,0 +1,305 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageSessionContextTestCase.java 1391005 2012-09-27 13:39:57Z mehdi $ */ + +package org.apache.xmlgraphics.image.loader; + +import java.io.File; +import java.io.FileNotFoundException; + +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; +import javax.xml.transform.Source; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.URIResolver; +import javax.xml.transform.dom.DOMResult; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.sax.SAXSource; +import javax.xml.transform.stream.StreamSource; + +import org.junit.Test; +import org.xml.sax.InputSource; +import org.xml.sax.XMLReader; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.apache.xmlgraphics.image.loader.util.ImageUtil; +import org.apache.xmlgraphics.io.XmlSourceUtil; + +/** + * Tests for AbstractImageSessionContext. + */ +public class ImageSessionContextTestCase { + + private MockImageContext imageContext = MockImageContext.getInstance(); + + @Test + public void testStreamSourceWithSystemID() throws Exception { + URIResolver resolver = new URIResolver() { + public Source resolve(String href, String base) throws TransformerException { + if (href.startsWith("img:")) { + String filename = href.substring(4); + return new StreamSource(base + filename); + } else { + return null; + } + } + }; + String uri = "img:asf-logo.png"; + + ImageSource imgSrc = checkImageInputStreamAvailable(uri, resolver); + assertTrue(imgSrc.isFastSource()); //Access through local file system + } + + @Test + public void testStreamSourceWithInputStream() throws Exception { + URIResolver resolver = new URIResolver() { + public Source resolve(String href, String base) throws TransformerException { + if (href.startsWith("img:")) { + String filename = href.substring(4); + try { + return new StreamSource(new java.io.FileInputStream( + new File(MockImageSessionContext.IMAGE_BASE_DIR, filename))); + } catch (FileNotFoundException e) { + throw new TransformerException(e); + } + } else { + return null; + } + } + }; + String uri = "img:asf-logo.png"; + + ImageSource imgSrc = checkImageInputStreamAvailable(uri, resolver); + //We don't pass in the URI, so no fast source is possible + assertTrue(!imgSrc.isFastSource()); + } + + @Test + public void testStreamSourceWithFile() throws Exception { + URIResolver resolver = new URIResolver() { + public Source resolve(String href, String base) throws TransformerException { + if (href.startsWith("img:")) { + String filename = href.substring(4); + File f = new File(MockImageSessionContext.IMAGE_BASE_DIR, filename); + return new StreamSource(f); + } else { + return null; + } + } + }; + String uri = "img:asf-logo.png"; + + ImageSource imgSrc = checkImageInputStreamAvailable(uri, resolver); + assertTrue(imgSrc.isFastSource()); //Accessed through the local file system + } + + @Test + public void testStreamSourceWithInputStreamAndSystemID() throws Exception { + URIResolver resolver = new URIResolver() { + public Source resolve(String href, String base) throws TransformerException { + if (href.startsWith("img:")) { + String filename = href.substring(4); + try { + File f = new File(MockImageSessionContext.IMAGE_BASE_DIR, filename); + return new StreamSource( + new java.io.FileInputStream(f), + f.toURI().toASCIIString()); + } catch (FileNotFoundException e) { + throw new TransformerException(e); + } + } else { + return null; + } + } + }; + String uri = "img:asf-logo.png"; + + ImageSource imgSrc = checkImageInputStreamAvailable(uri, resolver); + assertTrue(imgSrc.isFastSource()); //Access through local file system (thanks to the URI + //being passed through by the URIResolver) + } + + @Test + public void testStreamSourceWithReader() throws Exception { + URIResolver resolver = new URIResolver() { + public Source resolve(String href, String base) throws TransformerException { + if (href.startsWith("img:")) { + String filename = href.substring(4); + return new StreamSource(new java.io.StringReader(filename)); + } else { + return null; + } + } + }; + String uri = "img:asf-logo.png"; + + Source src = resolve(uri, resolver); + assertTrue(src instanceof StreamSource); //Source remains unchanged + assertTrue(XmlSourceUtil.hasReader(src)); + } + + private ImageSource checkImageInputStreamAvailable(String uri, URIResolver resolver) { + Source src = resolve(uri, resolver); + assertNotNull("Source must not be null", src); + assertTrue("Source must be an ImageSource", src instanceof ImageSource); + ImageSource imgSrc = (ImageSource) src; + assertTrue(ImageUtil.hasImageInputStream(imgSrc)); + return imgSrc; + } + + private Source resolve(String uri, URIResolver resolver) { + ImageSessionContext sessionContext = new SimpleURIResolverBasedImageSessionContext( + imageContext, MockImageSessionContext.IMAGE_BASE_DIR, resolver); + Source src = sessionContext.newSource(uri); + return src; + } + + @Test + public void testSAXSourceWithSystemID() throws Exception { + URIResolver resolver = new URIResolver() { + public Source resolve(String href, String base) throws TransformerException { + if (href.startsWith("img:")) { + String filename = href.substring(4); + InputSource is = new InputSource(base + filename); + return new SAXSource(is); + } else { + return null; + } + } + }; + String uri = "img:asf-logo.png"; + + ImageSource imgSrc = checkImageInputStreamAvailable(uri, resolver); + assertTrue(imgSrc.isFastSource()); + } + + @Test + public void testSAXSourceWithInputStream() throws Exception { + URIResolver resolver = new URIResolver() { + public Source resolve(String href, String base) throws TransformerException { + if (href.startsWith("img:")) { + String filename = href.substring(4); + InputSource is; + try { + is = new InputSource(new java.io.FileInputStream( + new File(MockImageSessionContext.IMAGE_BASE_DIR, filename))); + } catch (FileNotFoundException e) { + throw new TransformerException(e); + } + return new SAXSource(is); + } else { + return null; + } + } + }; + String uri = "img:asf-logo.png"; + + checkImageInputStreamAvailable(uri, resolver); + } + + @Test + public void testSAXSourceWithReader() throws Exception { + URIResolver resolver = new URIResolver() { + public Source resolve(String href, String base) throws TransformerException { + if (href.startsWith("img:")) { + String filename = href.substring(4); + InputSource is; + is = new InputSource(new java.io.StringReader(filename)); + return new SAXSource(is); + } else { + return null; + } + } + }; + String uri = "img:asf-logo.png"; + + Source src = resolve(uri, resolver); + assertTrue(src instanceof SAXSource); //Source remains unchanged + assertTrue(XmlSourceUtil.hasReader(src)); + } + + private static final String SOME_XML = "Hello World!"; + + @Test + public void testSAXSourceWithXMLReader() throws Exception { + URIResolver resolver = new URIResolver() { + public Source resolve(String href, String base) throws TransformerException { + if (href.startsWith("xml:")) { + String xml = href.substring(4); + InputSource is = new InputSource(new java.io.StringReader(xml)); + return new SAXSource(createSomeXMLReader(), is); + } else { + return null; + } + } + }; + String uri = "xml:" + SOME_XML; + + Source src = resolve(uri, resolver); + assertTrue(src instanceof SAXSource); //Source remains unchanged + SAXSource saxSrc = (SAXSource) src; + assertNotNull(saxSrc.getXMLReader()); + assertNotNull(saxSrc.getInputSource()); + } + + @Test + public void testDOMSource() throws Exception { + URIResolver resolver = new URIResolver() { + public Source resolve(String href, String base) throws TransformerException { + if (href.startsWith("xml:")) { + String xml = href.substring(4); + InputSource is = new InputSource(new java.io.StringReader(xml)); + SAXSource sax = new SAXSource(createSomeXMLReader(), is); + + //Convert SAXSource to DOMSource + TransformerFactory tFactory = TransformerFactory.newInstance(); + Transformer transformer = tFactory.newTransformer(); + DOMResult res = new DOMResult(); + transformer.transform(sax, res); + return new DOMSource(res.getNode()); + } else { + return null; + } + } + }; + String uri = "xml:" + SOME_XML; + + Source src = resolve(uri, resolver); + assertTrue(src instanceof DOMSource); //Source remains unchanged + DOMSource domSrc = (DOMSource) src; + assertNotNull(domSrc.getNode()); + } + + private XMLReader createSomeXMLReader() { + SAXParserFactory parserFactory = SAXParserFactory.newInstance(); + SAXParser parser; + try { + parser = parserFactory.newSAXParser(); + return parser.getXMLReader(); + } catch (Exception e) { + fail("Could not create XMLReader"); + return null; + } + } + +} diff --git a/src/test/java/org/apache/xmlgraphics/image/loader/ImageUtilTestCase.java b/src/test/java/org/apache/xmlgraphics/image/loader/ImageUtilTestCase.java new file mode 100644 index 0000000..26f3fe0 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/image/loader/ImageUtilTestCase.java @@ -0,0 +1,89 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageUtilTestCase.java 1365650 2012-07-25 15:59:30Z mehdi $ */ + +package org.apache.xmlgraphics.image.loader; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +import org.apache.xmlgraphics.image.loader.util.ImageUtil; + +/** + * Tests for the ImageUtil class. + */ +public class ImageUtilTestCase { + + /** + * Tests {@link ImageUtil.needPageIndexFromURI(String)}. + * @throws Exception if an error occurs + */ + @Test + public void testNeedPageIndex() throws Exception { + int pageIndex; + + pageIndex = ImageUtil.needPageIndexFromURI("http://localhost/images/scan1.tif"); + assertEquals(0, pageIndex); + pageIndex = ImageUtil.needPageIndexFromURI("http://localhost/images/scan1.tif#page=3"); + assertEquals(2, pageIndex); + pageIndex = ImageUtil.needPageIndexFromURI("http://localhost/images/scan1.tif#page=0"); + assertEquals(0, pageIndex); + pageIndex = ImageUtil.needPageIndexFromURI("http://localhost/images/scan1.tif#page="); + assertEquals(0, pageIndex); + pageIndex = ImageUtil.needPageIndexFromURI("http://localhost/images/scan1.tif#page=x"); + assertEquals(0, pageIndex); + pageIndex = ImageUtil.needPageIndexFromURI("http://localhost/images/scan1.tif#page=-1"); + assertEquals(0, pageIndex); + pageIndex = ImageUtil.needPageIndexFromURI("#page=2"); + assertEquals(1, pageIndex); + + //Not a valid URI + try { + pageIndex = ImageUtil.needPageIndexFromURI("C:\\images\\scan1.tif#page=44"); + fail("Invalid URI. Method must fail."); + } catch (IllegalArgumentException e) { + //expected + } + //Valid URI + pageIndex = ImageUtil.needPageIndexFromURI("file:///C:/images/scan1.tif#page=44"); + assertEquals(43, pageIndex); + } + + /** + * Tests {@link ImageUtil.getPageIndexFromURI(String)}. + * @throws Exception if an error occurs + */ + @Test + public void testGetPageIndex() throws Exception { + Integer pageIndex; + + pageIndex = ImageUtil.getPageIndexFromURI("http://localhost/images/scan1.tif"); + assertNull(pageIndex); + pageIndex = ImageUtil.getPageIndexFromURI("http://localhost/images/scan1.tif#page=3"); + assertEquals(2, pageIndex.intValue()); + //Note: no detailed test anymore as this is tested through needPageIndexFromURI(). + + //getPageIndexFromURI only works on URIs, so ignore anything that doesn't have a '#' + pageIndex = ImageUtil.getPageIndexFromURI("C:\\Temp\\scan1.tif"); + assertNull(pageIndex); + } + +} diff --git a/src/test/java/org/apache/xmlgraphics/image/loader/MockImageContext.java b/src/test/java/org/apache/xmlgraphics/image/loader/MockImageContext.java new file mode 100644 index 0000000..1b7bba7 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/image/loader/MockImageContext.java @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: MockImageContext.java 924666 2010-03-18 08:26:30Z jeremias $ */ + +package org.apache.xmlgraphics.image.loader; + +import org.apache.xmlgraphics.image.loader.impl.ImageConverterBitmap2G2D; +import org.apache.xmlgraphics.image.loader.impl.ImageConverterBuffered2Rendered; +import org.apache.xmlgraphics.image.loader.impl.ImageConverterG2D2Bitmap; +import org.apache.xmlgraphics.image.loader.impl.ImageLoaderFactoryInternalTIFF; +import org.apache.xmlgraphics.image.loader.impl.ImageLoaderFactoryRawCCITTFax; +import org.apache.xmlgraphics.image.loader.impl.PreloaderEPS; +import org.apache.xmlgraphics.image.loader.impl.PreloaderJPEG; +import org.apache.xmlgraphics.image.loader.impl.PreloaderTIFF; +import org.apache.xmlgraphics.image.loader.spi.ImageImplRegistry; + +/** + * Mock implementation for testing. + */ +public class MockImageContext implements ImageContext { + + private static MockImageContext instance; + + private ImageManager imageManager; + + /** + * Returns a singleton instance of the mock image context. + * @return the singleton + */ + public static MockImageContext getInstance() { + if (instance == null) { + instance = new MockImageContext(true); + } + return instance; + } + + /** + * Returns an image context for testing that only contains platform- and classpath-independent + * implementations so consistent test results can be obtained irrespective of the test + * environment. + * @return a new image context + */ + public static MockImageContext newSafeInstance() { + MockImageContext ic = new MockImageContext(false); + ImageImplRegistry registry = ic.getImageManager().getRegistry(); + registry.registerPreloader(new PreloaderTIFF()); + registry.registerPreloader(new PreloaderJPEG()); + registry.registerPreloader(new PreloaderEPS()); + registry.registerLoaderFactory(new ImageLoaderFactoryInternalTIFF()); + registry.registerLoaderFactory(new ImageLoaderFactoryRawCCITTFax()); + registry.registerConverter(new ImageConverterBitmap2G2D()); + registry.registerConverter(new ImageConverterG2D2Bitmap()); + registry.registerConverter(new ImageConverterBuffered2Rendered()); + return ic; + } + + /** + * Creates a new mock image context. + * @param discover true to enable plug-in discovery + */ + public MockImageContext(boolean discover) { + this.imageManager = new ImageManager(new ImageImplRegistry(discover), this); + } + + /** {@inheritDoc} */ + public float getSourceResolution() { + return 72; + } + + /** + * Returns the image manager. + * @return the image manager + */ + public ImageManager getImageManager() { + return this.imageManager; + } + + /** + * Creates a new image session context. + * @return the image session context + */ + public ImageSessionContext newSessionContext() { + return new MockImageSessionContext(this); + } +} diff --git a/src/test/java/org/apache/xmlgraphics/image/loader/MockImageSessionContext.java b/src/test/java/org/apache/xmlgraphics/image/loader/MockImageSessionContext.java new file mode 100644 index 0000000..8dd722a --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/image/loader/MockImageSessionContext.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: MockImageSessionContext.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.image.loader; + +import java.io.File; + +import org.apache.xmlgraphics.image.loader.impl.DefaultImageSessionContext; + +/** + * Mock implementation for testing. + */ +public class MockImageSessionContext extends DefaultImageSessionContext { + + public static final File IMAGE_BASE_DIR = new File("./test/images/"); + + public MockImageSessionContext(ImageContext context) { + super(context, IMAGE_BASE_DIR); + } + + /** {@inheritDoc} */ + public float getTargetResolution() { + return 300; + } + +} diff --git a/src/test/java/org/apache/xmlgraphics/image/loader/ObservableInputStream.java b/src/test/java/org/apache/xmlgraphics/image/loader/ObservableInputStream.java new file mode 100644 index 0000000..b8940b8 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/image/loader/ObservableInputStream.java @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ObservableInputStream.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.image.loader; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * This is a proxying input stream that records whether a stream has been closed or not. + */ +public class ObservableInputStream extends FilterInputStream implements ObservableStream { + + /** logger */ + protected static Log log = LogFactory.getLog(ObservableInputStream.class); + + private boolean closed; + private String systemID; + + /** + * Main constructor. + * @param in the underlying input stream + * @param systemID the system ID for the input stream for reference + */ + public ObservableInputStream(InputStream in, String systemID) { + super(in); + this.systemID = systemID; + } + + /** {@inheritDoc} */ + public void close() throws IOException { + if (!closed) { + log.debug("Stream is being closed: " + getSystemID()); + try { + this.in.close(); + } catch (IOException ioe) { + log.error("Error while closing underlying stream: " + ioe.getMessage()); + } + closed = true; + } else { + throw new IllegalStateException("Stream is already closed!"); + } + } + + /** {@inheritDoc} */ + public boolean isClosed() { + return this.closed; + } + + /** {@inheritDoc} */ + public String getSystemID() { + return this.systemID; + } + +} diff --git a/src/test/java/org/apache/xmlgraphics/image/loader/ObservableStream.java b/src/test/java/org/apache/xmlgraphics/image/loader/ObservableStream.java new file mode 100644 index 0000000..d0d39bb --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/image/loader/ObservableStream.java @@ -0,0 +1,109 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ObservableStream.java 1365650 2012-07-25 15:59:30Z mehdi $ */ + +package org.apache.xmlgraphics.image.loader; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +import javax.imageio.stream.ImageInputStream; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Implemented by observable streams. + */ +public interface ObservableStream { + + /** + * Indicates whether the stream has been closed. + * @return true if the stream is closed + */ + boolean isClosed(); + + /** + * Returns the system ID for the stream being observed. + * @return the system ID + */ + String getSystemID(); + + public static class Factory { + + public static ImageInputStream observe(ImageInputStream iin, String systemID) { + return (ImageInputStream) Proxy.newProxyInstance( + Factory.class.getClassLoader(), + new Class[] {ImageInputStream.class, ObservableStream.class}, + new ObservingImageInputStreamInvocationHandler(iin, systemID)); + } + + } + + public static class ObservingImageInputStreamInvocationHandler + implements InvocationHandler, ObservableStream { + + /** logger */ + protected static Log log = LogFactory.getLog(ObservableInputStream.class); + + private ImageInputStream iin; + private boolean closed; + private String systemID; + + public ObservingImageInputStreamInvocationHandler(ImageInputStream iin, String systemID) { + this.iin = iin; + this.systemID = systemID; + } + + /** {@inheritDoc} */ + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + if (method.getDeclaringClass().equals(ObservableStream.class)) { + return method.invoke(this, args); + } else if ("close".equals(method.getName())) { + if (!closed) { + log.debug("Stream is being closed: " + getSystemID()); + closed = true; + try { + return method.invoke(iin, args); + } catch (InvocationTargetException ite) { + log.error("Error while closing underlying stream: " + ite.getMessage()); + throw ite; + } + } else { + throw new IllegalStateException("Stream is already closed!"); + } + } else { + return method.invoke(iin, args); + } + } + + /** {@inheritDoc} */ + public String getSystemID() { + return this.systemID; + } + + /** {@inheritDoc} */ + public boolean isClosed() { + return this.closed; + } + + } + +} diff --git a/src/test/java/org/apache/xmlgraphics/image/loader/PenaltyTestCase.java b/src/test/java/org/apache/xmlgraphics/image/loader/PenaltyTestCase.java new file mode 100644 index 0000000..cc40a9d --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/image/loader/PenaltyTestCase.java @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PenaltyTestCase.java 1365650 2012-07-25 15:59:30Z mehdi $ */ + +package org.apache.xmlgraphics.image.loader; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import org.apache.xmlgraphics.image.loader.util.Penalty; + +/** + * Tests for the {@link Penalty}. + */ +public class PenaltyTestCase { + + /** + * Tests for penalty handling. + * @throws Exception if an error occurs + */ + @Test + public void testTruncatePenalty() throws Exception { + assertEquals(0, Penalty.truncate(0)); + long penalty = Integer.MAX_VALUE; + assertEquals(Integer.MAX_VALUE, Penalty.truncate(penalty)); + + //Force integer wrap-around + penalty++; + assertEquals(Integer.MAX_VALUE, Penalty.truncate(penalty)); + //For comparison, normal casting does this + assertEquals(Integer.MIN_VALUE, (int) penalty); + + //Now on the other end of the spectrum... + penalty = Integer.MIN_VALUE; + assertEquals(Integer.MIN_VALUE, Penalty.truncate(penalty)); + + //Force integer wrap-around + penalty -= 500; + assertEquals(Integer.MIN_VALUE, Penalty.truncate(penalty)); + //For comparison, normal casting does this + assertEquals(Integer.MAX_VALUE - 499, (int) penalty); + } + + /** + * Tests for the {@link Penalty} class. + * @throws Exception if an error occurs + */ + @Test + public void testPenalty() throws Exception { + Penalty p1 = Penalty.toPenalty(100); + assertEquals(100, p1.getValue()); + Penalty p2 = p1.add(Penalty.toPenalty(50)); + assertEquals(150, p2.getValue()); + + p1 = Penalty.toPenalty(0); + assertEquals(0, p1.getValue()); + + p1 = Penalty.INFINITE_PENALTY; + assertEquals(Integer.MAX_VALUE, p1.getValue()); + assertTrue(p1.isInfinitePenalty()); + p2 = p1.add(p2); + assertEquals(Integer.MAX_VALUE, p2.getValue()); + assertTrue(p2.isInfinitePenalty()); + } + +} diff --git a/src/test/java/org/apache/xmlgraphics/image/loader/PipelineFactoryTestCase.java b/src/test/java/org/apache/xmlgraphics/image/loader/PipelineFactoryTestCase.java new file mode 100644 index 0000000..3741ee4 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/image/loader/PipelineFactoryTestCase.java @@ -0,0 +1,206 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PipelineFactoryTestCase.java 1681108 2015-05-22 13:26:12Z ssteiner $ */ + +package org.apache.xmlgraphics.image.loader; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +import org.apache.xmlgraphics.image.codec.tiff.TIFFImage; +import org.apache.xmlgraphics.image.loader.impl.ImageLoaderRawCCITTFax; +import org.apache.xmlgraphics.image.loader.mocks.MockImageLoaderFactoryTIFF; +import org.apache.xmlgraphics.image.loader.pipeline.ImageProviderPipeline; +import org.apache.xmlgraphics.image.loader.pipeline.PipelineFactory; +import org.apache.xmlgraphics.image.loader.spi.ImageImplRegistry; +import org.apache.xmlgraphics.image.loader.util.Penalty; + +/** + * Tests the pipeline factory. + */ +public class PipelineFactoryTestCase { + + /** + * Tests the pipeline factory by checking to load a TIFF image. + * @throws Exception if an error occurs + */ + @Test + public void testPipelineFactoryPlain() throws Exception { + MockImageContext imageContext = MockImageContext.newSafeInstance(); + ImageManager manager = imageContext.getImageManager(); + PipelineFactory pFactory = new PipelineFactory(manager); + + //Input is some TIFF file + ImageInfo imageInfo = new ImageInfo("test:tiff", "image/tiff"); + + //We want a G2D image + ImageFlavor targetFlavor = ImageFlavor.GRAPHICS2D; + + ImageProviderPipeline pipeline = pFactory.newImageConverterPipeline( + imageInfo, targetFlavor); + assertNotNull(pipeline); + assertEquals(pipeline.getTargetFlavor(), targetFlavor); + + //penalty for internal TIFF implementation (fallback role) is 1000 + 10 for the conversion + assertEquals(1010, pipeline.getConversionPenalty()); + assertEquals(ImageFlavor.GRAPHICS2D, pipeline.getTargetFlavor()); + if (pipeline.toString().indexOf("LoaderInternalTIFF") < 0) { + fail("Chose the wrong pipeline: " + pipeline.toString()); + } + if (pipeline.toString().indexOf("ImageConverterBitmap2G2D") < 0) { + fail("Chose the wrong pipeline: " + pipeline.toString()); + } + + ImageProviderPipeline[] candidates = pFactory.determineCandidatePipelines( + imageInfo, new ImageFlavor[] {targetFlavor}); + assertEquals(1, candidates.length); + + //Now add another implementation that poses as TIFF loader + imageContext.getImageManager().getRegistry().registerLoaderFactory( + new MockImageLoaderFactoryTIFF()); + + candidates = pFactory.determineCandidatePipelines( + imageInfo, targetFlavor); + assertEquals(3, candidates.length); + //3 because the mock impl provides Buffered- and RenderedImage capabilities + + pipeline = pFactory.newImageConverterPipeline(imageInfo, targetFlavor); + assertNotNull(pipeline); + assertEquals(pipeline.getTargetFlavor(), targetFlavor); + + //Assuming mock impl without penalty + 10 for the conversion + assertEquals(10, pipeline.getConversionPenalty()); + assertEquals(ImageFlavor.GRAPHICS2D, pipeline.getTargetFlavor()); + if (pipeline.toString().indexOf(MockImageLoaderFactoryTIFF.class.getName()) < 0) { + fail("Chose the wrong pipeline: " + pipeline.toString()); + } + if (pipeline.toString().indexOf("ImageConverterBitmap2G2D") < 0) { + fail("Chose the wrong pipeline: " + pipeline.toString()); + } + } + + /** + * Similar test as above but here we take raw CCITT loading into consideration, too. + * @throws Exception if an error occurs + */ + @Test + public void testPipelineFactoryImageInfoDependency() throws Exception { + MockImageContext imageContext = MockImageContext.newSafeInstance(); + ImageManager manager = imageContext.getImageManager(); + PipelineFactory pFactory = new PipelineFactory(manager); + + //Input is some TIFF file with CCITT Group 4 compression + ImageInfo imageInfo = new ImageInfo("test:tiff", "image/tiff"); + imageInfo.getCustomObjects().put("TIFF_STRIP_COUNT", 1); + imageInfo.getCustomObjects().put("TIFF_COMPRESSION", TIFFImage.COMP_FAX_G4_2D); + + //We want either a G2D image or a raw CCITT image + ImageFlavor[] targetFlavors = new ImageFlavor[] { + ImageFlavor.GRAPHICS2D, ImageFlavor.RAW_CCITTFAX}; + + ImageProviderPipeline[] candidates = pFactory.determineCandidatePipelines( + imageInfo, targetFlavors); + assertNotNull(candidates); + assertEquals(2, candidates.length); + + ImageProviderPipeline pipeline = manager.choosePipeline(candidates); + + //0 penalty because the raw loader is the most efficient choice here + assertEquals(0, pipeline.getConversionPenalty()); + assertEquals(ImageFlavor.RAW_CCITTFAX, pipeline.getTargetFlavor()); + if (pipeline.toString().indexOf("LoaderRawCCITTFax") < 0) { + fail("Chose the wrong pipeline: " + pipeline.toString()); + } + + //Now, we set this to a multi-strip TIFF which should disable the raw loader + imageInfo.getCustomObjects().put("TIFF_STRIP_COUNT", 7); + + candidates = pFactory.determineCandidatePipelines( + imageInfo, targetFlavors); + assertNotNull(candidates); + assertEquals(1, candidates.length); + + pipeline = manager.choosePipeline(candidates); + + //penalty for internal TIFF implementation (fallback role) is 1000 + 10 for the conversion + assertEquals(1010, pipeline.getConversionPenalty()); + assertEquals(ImageFlavor.GRAPHICS2D, pipeline.getTargetFlavor()); + if (pipeline.toString().indexOf("LoaderInternalTIFF") < 0) { + fail("Chose the wrong pipeline: " + pipeline.toString()); + } + if (pipeline.toString().indexOf("ImageConverterBitmap2G2D") < 0) { + fail("Chose the wrong pipeline: " + pipeline.toString()); + } + } + + /** + * Similar test as above but now we're playing with additional penalties in the registry. + * @throws Exception if an error occurs + */ + @Test + public void testPipelineFactoryAdditionalPenalty() throws Exception { + MockImageContext imageContext = MockImageContext.newSafeInstance(); + ImageManager manager = imageContext.getImageManager(); + PipelineFactory pFactory = new PipelineFactory(manager); + + //Adding additional penalty for CCITT loading + ImageImplRegistry registry = imageContext.getImageManager().getRegistry(); + registry.setAdditionalPenalty(ImageLoaderRawCCITTFax.class.getName(), + Penalty.toPenalty(10000)); + + //Input is some TIFF file with CCITT Group 4 compression + ImageInfo imageInfo = new ImageInfo("test:tiff", "image/tiff"); + imageInfo.getCustomObjects().put("TIFF_STRIP_COUNT", 1); + imageInfo.getCustomObjects().put("TIFF_COMPRESSION", TIFFImage.COMP_FAX_G4_2D); + + //We want either a G2D image or a raw CCITT image + ImageFlavor[] targetFlavors = new ImageFlavor[] { + ImageFlavor.GRAPHICS2D, ImageFlavor.RAW_CCITTFAX}; + + ImageProviderPipeline[] candidates = pFactory.determineCandidatePipelines( + imageInfo, targetFlavors); + assertNotNull(candidates); + assertEquals(2, candidates.length); + + ImageProviderPipeline pipeline = manager.choosePipeline(candidates); + + //penalty for internal TIFF implementation (fallback role) is 1000 + 10 for the conversion + assertEquals(1010, pipeline.getConversionPenalty()); + assertEquals(ImageFlavor.GRAPHICS2D, pipeline.getTargetFlavor()); + if (pipeline.toString().indexOf("LoaderInternalTIFF") < 0) { + fail("Chose the wrong pipeline: " + pipeline.toString()); + } + if (pipeline.toString().indexOf("ImageConverterBitmap2G2D") < 0) { + fail("Chose the wrong pipeline: " + pipeline.toString()); + } + + //Now set an infinite penalty making the solution ineligible + registry.setAdditionalPenalty(ImageLoaderRawCCITTFax.class.getName(), + Penalty.INFINITE_PENALTY); + + candidates = pFactory.determineCandidatePipelines(imageInfo, targetFlavors); + assertNotNull(candidates); + assertEquals(1, candidates.length); + //While earlier 2 candidates were returned, here we only get 1 because of the infinite + //penalty. + } + +} diff --git a/src/test/java/org/apache/xmlgraphics/image/loader/SimpleURIResolverBasedImageSessionContext.java b/src/test/java/org/apache/xmlgraphics/image/loader/SimpleURIResolverBasedImageSessionContext.java new file mode 100644 index 0000000..bdc7029 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/image/loader/SimpleURIResolverBasedImageSessionContext.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: SimpleURIResolverBasedImageSessionContext.java 1365650 2012-07-25 15:59:30Z mehdi $ */ + +package org.apache.xmlgraphics.image.loader; + +import java.io.File; + +import javax.xml.transform.Source; +import javax.xml.transform.TransformerException; +import javax.xml.transform.URIResolver; + +import org.apache.xmlgraphics.image.loader.impl.DefaultImageSessionContext; + +/** + * ImageSessionContext which uses a URIResolver to resolve URIs. + */ +public class SimpleURIResolverBasedImageSessionContext + extends DefaultImageSessionContext { + + private URIResolver resolver; + + /** + * Main constructor + * @param context the parent image context + * @param baseDir the base directory + * @param resolver the URI resolver + */ + public SimpleURIResolverBasedImageSessionContext(ImageContext context, + File baseDir, URIResolver resolver) { + super(context, baseDir); + this.resolver = resolver; + } + + /** {@inheritDoc} */ + protected Source resolveURI(String uri) { + try { + return this.resolver.resolve(uri, getBaseDir().toURI().toASCIIString()); + } catch (TransformerException e) { + e.printStackTrace(); + return null; + } + } +} diff --git a/src/test/java/org/apache/xmlgraphics/image/loader/cache/DefaultExpirationPolicyTestCase.java b/src/test/java/org/apache/xmlgraphics/image/loader/cache/DefaultExpirationPolicyTestCase.java new file mode 100644 index 0000000..69c9e63 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/image/loader/cache/DefaultExpirationPolicyTestCase.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DefaultExpirationPolicyTestCase.java 1365650 2012-07-25 15:59:30Z mehdi $ */ + +package org.apache.xmlgraphics.image.loader.cache; + +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + + +/** + * Tests {@link DefaultExpirationPolicy}. + */ +public class DefaultExpirationPolicyTestCase { + + /** + * Never expire. + * @throws Exception if an error occurs + */ + @Test + public void testNeverExpire() throws Exception { + ExpirationPolicy policy; + policy = new DefaultExpirationPolicy(DefaultExpirationPolicy.EXPIRATION_NEVER); + + MockTimeStampProvider provider = new MockTimeStampProvider(); + + long ts = 1000000; + assertFalse(policy.isExpired(provider, ts)); + provider.setTimeStamp(ts + Integer.MAX_VALUE); + assertFalse(policy.isExpired(provider, ts)); + } + + /** + * Normal expiration + * @throws Exception if an error occurs + */ + @Test + public void testNormalExpiration() throws Exception { + ExpirationPolicy policy; + policy = new DefaultExpirationPolicy(2); + + MockTimeStampProvider provider = new MockTimeStampProvider(); + + long ts = 1000000; + provider.setTimeStamp(ts); + assertFalse(policy.isExpired(provider, ts)); + provider.setTimeStamp(ts + 1000); + assertFalse(policy.isExpired(provider, ts)); + + provider.setTimeStamp(ts + 2000); + assertTrue(policy.isExpired(provider, ts)); + provider.setTimeStamp(ts + 3000); + assertTrue(policy.isExpired(provider, ts)); + } + +} diff --git a/src/test/java/org/apache/xmlgraphics/image/loader/cache/ImageCacheLoggingStatistics.java b/src/test/java/org/apache/xmlgraphics/image/loader/cache/ImageCacheLoggingStatistics.java new file mode 100644 index 0000000..6843084 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/image/loader/cache/ImageCacheLoggingStatistics.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageCacheLoggingStatistics.java 750418 2009-03-05 11:03:54Z vhennebert $ */ + +package org.apache.xmlgraphics.image.loader.cache; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * ImageCacheListener implementation for debugging purposes. + */ +public class ImageCacheLoggingStatistics extends ImageCacheStatistics { + + /** logger */ + protected static Log log = LogFactory.getLog(ImageCacheLoggingStatistics.class); + + /** + * Main constructor. + * @param detailed true if statistics for each URI should be kept. + */ + public ImageCacheLoggingStatistics(boolean detailed) { + super(detailed); + } + + /** {@inheritDoc} */ + public void invalidHit(String uri) { + super.invalidHit(uri); + log.info("Invalid HIT: " + uri); + } + + /** {@inheritDoc} */ + public void cacheHitImage(ImageKey key) { + super.cacheHitImage(key); + log.info("Image Cache HIT: " + key); + } + + /** {@inheritDoc} */ + public void cacheHitImageInfo(String uri) { + super.cacheHitImageInfo(uri); + log.info("ImageInfo Cache HIT: " + uri); + } + + /** {@inheritDoc} */ + public void cacheMissImage(ImageKey key) { + super.cacheMissImage(key); + log.info("Image Cache MISS: " + key); + } + + /** {@inheritDoc} */ + public void cacheMissImageInfo(String uri) { + super.cacheMissImageInfo(uri); + log.info("ImageInfo Cache MISS: " + uri); + } + + + +} diff --git a/src/test/java/org/apache/xmlgraphics/image/loader/cache/ImageCacheTestCase.java b/src/test/java/org/apache/xmlgraphics/image/loader/cache/ImageCacheTestCase.java new file mode 100644 index 0000000..14a83e5 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/image/loader/cache/ImageCacheTestCase.java @@ -0,0 +1,208 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageCacheTestCase.java 1365650 2012-07-25 15:59:30Z mehdi $ */ + +package org.apache.xmlgraphics.image.loader.cache; + +import java.io.FileNotFoundException; + +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.apache.xmlgraphics.image.loader.ImageFlavor; +import org.apache.xmlgraphics.image.loader.ImageInfo; +import org.apache.xmlgraphics.image.loader.ImageManager; +import org.apache.xmlgraphics.image.loader.ImageSessionContext; +import org.apache.xmlgraphics.image.loader.MockImageContext; +import org.apache.xmlgraphics.image.loader.impl.ImageBuffered; + +/** + * Tests for bundled ImageLoader implementations. + */ +public class ImageCacheTestCase { + + private static final boolean DEBUG = false; + + private MockImageContext imageContext = MockImageContext.getInstance(); + private ImageSessionContext sessionContext = imageContext.newSessionContext(); + private ImageManager manager = imageContext.getImageManager(); + private ImageCacheStatistics statistics = (DEBUG + ? new ImageCacheLoggingStatistics(true) : new ImageCacheStatistics(true)); + + /** {@inheritDoc} */ + @Before + public void setUp() throws Exception { + manager.getCache().clearCache(); + statistics.reset(); + manager.getCache().setCacheListener(statistics); + } + + /** + * Tests the ImageInfo cache. + * @throws Exception if an error occurs + */ + @Test + public void testImageInfoCache() throws Exception { + String invalid1 = "invalid1.jpg"; + String invalid2 = "invalid2.jpg"; + String valid1 = "bgimg300dpi.bmp"; + String valid2 = "big-image.png"; + + + ImageInfo info1; + ImageInfo info2; + info1 = manager.getImageInfo(valid1, sessionContext); + assertNotNull(info1); + assertEquals(valid1, info1.getOriginalURI()); + + try { + manager.getImageInfo(invalid1, sessionContext); + fail("Expected FileNotFoundException for invalid URI"); + } catch (FileNotFoundException e) { + //expected + } + + //2 requests: + assertEquals(0, statistics.getImageInfoCacheHits()); + assertEquals(2, statistics.getImageInfoCacheMisses()); + assertEquals(0, statistics.getInvalidHits()); + statistics.reset(); + + //Cache Hit + info1 = manager.getImageInfo(valid1, sessionContext); + assertNotNull(info1); + assertEquals(valid1, info1.getOriginalURI()); + + //Cache Miss + info2 = manager.getImageInfo(valid2, sessionContext); + assertNotNull(info2); + assertEquals(valid2, info2.getOriginalURI()); + + try { + //Invalid Hit + manager.getImageInfo(invalid1, sessionContext); + fail("Expected FileNotFoundException for invalid URI"); + } catch (FileNotFoundException e) { + //expected + } + try { + //Invalid (Cache Miss) + manager.getImageInfo(invalid2, sessionContext); + fail("Expected FileNotFoundException for invalid URI"); + } catch (FileNotFoundException e) { + //expected + } + + //4 requests: + assertEquals(1, statistics.getImageInfoCacheHits()); + assertEquals(2, statistics.getImageInfoCacheMisses()); + assertEquals(1, statistics.getInvalidHits()); + statistics.reset(); + } + + @Test + public void testInvalidURIExpiration() throws Exception { + MockTimeStampProvider provider = new MockTimeStampProvider(); + ImageCache cache = new ImageCache(provider, new DefaultExpirationPolicy(2)); + cache.setCacheListener(statistics); + + String invalid1 = "invalid1.jpg"; + String invalid2 = "invalid2.jpg"; + String valid1 = "valid1.jpg"; + + provider.setTimeStamp(1000); + cache.registerInvalidURI(invalid1); + provider.setTimeStamp(1100); + cache.registerInvalidURI(invalid2); + + assertEquals(0, statistics.getInvalidHits()); + + //not expired, yet + provider.setTimeStamp(1200); + assertFalse(cache.isInvalidURI(valid1)); + assertTrue(cache.isInvalidURI(invalid1)); + assertTrue(cache.isInvalidURI(invalid2)); + assertEquals(2, statistics.getInvalidHits()); + + //first expiration time reached + provider.setTimeStamp(3050); + assertFalse(cache.isInvalidURI(valid1)); + assertFalse(cache.isInvalidURI(invalid1)); + assertTrue(cache.isInvalidURI(invalid2)); + assertEquals(3, statistics.getInvalidHits()); + + //second expiration time reached + provider.setTimeStamp(3200); + assertFalse(cache.isInvalidURI(valid1)); + assertFalse(cache.isInvalidURI(invalid1)); + assertFalse(cache.isInvalidURI(invalid2)); + assertEquals(3, statistics.getInvalidHits()); + } + + /** + * Tests the image cache reusing a cacheable Image created by the ImageLoader. + * @throws Exception if an error occurs + */ + @Test + public void testImageCache1() throws Exception { + String valid1 = "bgimg72dpi.gif"; + + ImageInfo info = manager.getImageInfo(valid1, sessionContext); + assertNotNull(info); + + ImageBuffered img1 = (ImageBuffered) manager.getImage( + info, ImageFlavor.BUFFERED_IMAGE, sessionContext); + assertNotNull(img1); + assertNotNull(img1.getBufferedImage()); + + ImageBuffered img2 = (ImageBuffered) manager.getImage( + info, ImageFlavor.BUFFERED_IMAGE, sessionContext); + //ImageBuffered does not have to be the same instance but we want at least the + //BufferedImage to be reused. + assertTrue("BufferedImage must be reused", + img1.getBufferedImage() == img2.getBufferedImage()); + + assertEquals(1, statistics.getImageCacheHits()); + assertEquals(1, statistics.getImageCacheMisses()); + } + + + /** + * Test to check if doInvalidURIHouseKeeping() throws a + * ConcurrentModificationException. + */ + @Test + public void testImageCacheHouseKeeping() { + ImageCache imageCache = new ImageCache(new TimeStampProvider(), + new DefaultExpirationPolicy(1)); + imageCache.registerInvalidURI("invalid"); + imageCache.registerInvalidURI("invalid2"); + try { + Thread.sleep(1200); + } catch (InterruptedException e) { + e.printStackTrace(); + } + imageCache.doHouseKeeping(); + } +} diff --git a/src/test/java/org/apache/xmlgraphics/image/loader/cache/MockTimeStampProvider.java b/src/test/java/org/apache/xmlgraphics/image/loader/cache/MockTimeStampProvider.java new file mode 100644 index 0000000..90f4e6c --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/image/loader/cache/MockTimeStampProvider.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: MockTimeStampProvider.java 759144 2009-03-27 14:16:18Z jeremias $ */ + +package org.apache.xmlgraphics.image.loader.cache; + +/** + * Mock subclass of the TimeStampProvider. + */ +class MockTimeStampProvider extends TimeStampProvider { + + private long timestamp; + + public MockTimeStampProvider() { + this(0); + } + + public MockTimeStampProvider(long timestamp) { + setTimeStamp(timestamp); + } + + public void setTimeStamp(long timestamp) { + this.timestamp = timestamp; + } + + /** {@inheritDoc} */ + public long getTimeStamp() { + return this.timestamp; + } + +} diff --git a/src/test/java/org/apache/xmlgraphics/image/loader/impl/ImageConverterG2D2BitmapTestCase.java b/src/test/java/org/apache/xmlgraphics/image/loader/impl/ImageConverterG2D2BitmapTestCase.java new file mode 100644 index 0000000..29c5001 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/image/loader/impl/ImageConverterG2D2BitmapTestCase.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ +package org.apache.xmlgraphics.image.loader.impl; + +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.geom.Rectangle2D; +import java.util.HashMap; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.xmlgraphics.image.loader.ImageInfo; +import org.apache.xmlgraphics.image.loader.ImageProcessingHints; +import org.apache.xmlgraphics.image.loader.ImageSize; +import org.apache.xmlgraphics.java2d.Graphics2DImagePainter; +import org.apache.xmlgraphics.java2d.color.DeviceCMYKColorSpace; + +public class ImageConverterG2D2BitmapTestCase { + @Test + public void testConvert() { + ImageInfo info = new ImageInfo(null, null); + ImageSize imageSize = new ImageSize(100, 100, 72); + imageSize.calcSizeFromPixels(); + info.setSize(imageSize); + HashMap hints = new HashMap(); + hints.put(ImageProcessingHints.TRANSPARENCY_INTENT, ImageProcessingHints.TRANSPARENCY_INTENT_IGNORE); + hints.put("CMYK", true); + ImageBuffered image = (ImageBuffered) new ImageConverterG2D2Bitmap().convert( + new ImageGraphics2D(info, new EmptyGraphics2DImagePainter()), hints); + Assert.assertEquals(image.getBufferedImage().getColorModel().getNumColorComponents(), 4); + Assert.assertEquals(image.getColorSpace().getClass(), DeviceCMYKColorSpace.class); + } + + private static class EmptyGraphics2DImagePainter implements Graphics2DImagePainter { + public void paint(Graphics2D g2d, Rectangle2D area) { + } + + public Dimension getImageSize() { + return null; + } + } +} diff --git a/src/test/java/org/apache/xmlgraphics/image/loader/impl/ImageLoaderFactoryPNGTestCase.java b/src/test/java/org/apache/xmlgraphics/image/loader/impl/ImageLoaderFactoryPNGTestCase.java new file mode 100644 index 0000000..0baf725 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/image/loader/impl/ImageLoaderFactoryPNGTestCase.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageLoaderFactoryPNGTestCase.java 1732019 2016-02-24 05:01:10Z gadams $ */ + +package org.apache.xmlgraphics.image.loader.impl; + +import org.junit.Test; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.apache.xmlgraphics.image.loader.ImageFlavor; +import org.apache.xmlgraphics.image.loader.spi.ImageLoader; +import org.apache.xmlgraphics.util.MimeConstants; + +public class ImageLoaderFactoryPNGTestCase { + + private ImageLoaderFactoryPNG ilfpng = new ImageLoaderFactoryPNG(); + + @Test + public void testGetSupportedMIMETypes() { + assertArrayEquals(new String[] {MimeConstants.MIME_PNG}, ilfpng.getSupportedMIMETypes()); + } + + @Test + public void testGetSupportedFlavors() { + assertArrayEquals(new ImageFlavor[] {ImageFlavor.RENDERED_IMAGE}, + ilfpng.getSupportedFlavors(MimeConstants.MIME_PNG)); + try { + ilfpng.getSupportedFlavors(MimeConstants.MIME_JPEG); + fail("An exception should have been thrown above...."); + } catch (IllegalArgumentException e) { + // do nothing; this is expected + } + } + + @Test + public void testNewImageLoader() { + ImageLoader il = ilfpng.newImageLoader(ImageFlavor.RENDERED_IMAGE); + assertTrue(il instanceof ImageLoaderPNG); + } + + @Test + public void testIsAvailable() { + assertTrue(ilfpng.isAvailable()); + } + +} diff --git a/src/test/java/org/apache/xmlgraphics/image/loader/impl/ImageLoaderImageIOTestCase.java b/src/test/java/org/apache/xmlgraphics/image/loader/impl/ImageLoaderImageIOTestCase.java new file mode 100644 index 0000000..28fcf14 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/image/loader/impl/ImageLoaderImageIOTestCase.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageLoaderImageIOTestCase.java 1365650 2012-07-25 15:59:30Z mehdi $ */ + +package org.apache.xmlgraphics.image.loader.impl; + +import java.net.URL; + +import org.junit.Test; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import org.apache.xmlgraphics.image.loader.Image; +import org.apache.xmlgraphics.image.loader.ImageContext; +import org.apache.xmlgraphics.image.loader.ImageFlavor; +import org.apache.xmlgraphics.image.loader.ImageInfo; +import org.apache.xmlgraphics.image.loader.ImageSessionContext; +import org.apache.xmlgraphics.image.loader.MockImageContext; +import org.apache.xmlgraphics.image.loader.MockImageSessionContext; +import org.apache.xmlgraphics.image.loader.impl.imageio.ImageLoaderImageIO; +import org.apache.xmlgraphics.util.MimeConstants; + +/** + * Tests for {@link ImageLoaderImageIO}. + */ +public class ImageLoaderImageIOTestCase { + + /** + * Tests a grayscale PNG that has a CMYK color profile. ImageLoaderImageIO used + * to fail on that with an IllegalArgumentException. + * @throws Exception if an error occurs + */ + @Test + public void testGrayPNGWithCMYKProfile() throws Exception { + URL imageURL = getClass().getResource("gray-vs-cmyk-profile.png"); + assertNotNull(imageURL); + String uri = imageURL.toURI().toASCIIString(); + + ImageLoaderImageIO loader = new ImageLoaderImageIO(ImageFlavor.RENDERED_IMAGE); + ImageContext context = MockImageContext.newSafeInstance(); + ImageSessionContext session = new MockImageSessionContext(context); + ImageInfo info = new ImageInfo(uri, MimeConstants.MIME_PNG); + Image im = loader.loadImage(info, null, session); + assertTrue(im instanceof ImageRendered); + } + +} diff --git a/src/test/java/org/apache/xmlgraphics/image/loader/impl/ImageLoaderPNGTestCase.java b/src/test/java/org/apache/xmlgraphics/image/loader/impl/ImageLoaderPNGTestCase.java new file mode 100644 index 0000000..76978cf --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/image/loader/impl/ImageLoaderPNGTestCase.java @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageLoaderPNGTestCase.java 1732019 2016-02-24 05:01:10Z gadams $ */ + +package org.apache.xmlgraphics.image.loader.impl; + +import java.io.IOException; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import org.apache.xmlgraphics.image.loader.Image; +import org.apache.xmlgraphics.image.loader.ImageContext; +import org.apache.xmlgraphics.image.loader.ImageException; +import org.apache.xmlgraphics.image.loader.ImageFlavor; +import org.apache.xmlgraphics.image.loader.ImageInfo; +import org.apache.xmlgraphics.image.loader.ImageSessionContext; +import org.apache.xmlgraphics.image.loader.MockImageContext; +import org.apache.xmlgraphics.image.loader.MockImageSessionContext; +import org.apache.xmlgraphics.util.MimeConstants; + +public class ImageLoaderPNGTestCase { + + private ImageLoaderPNG ilpng = new ImageLoaderPNG(); + + @Test + public void testGetUsagePenalty() { + assertEquals(1000, ilpng.getUsagePenalty()); + } + + @Test + public void testLoadImageImageInfoMapImageSessionContext() throws ImageException, IOException { + ImageContext context = MockImageContext.newSafeInstance(); + ImageSessionContext session = new MockImageSessionContext(context); + ImageInfo info = new ImageInfo("basn2c08.png", MimeConstants.MIME_PNG); + Image im = ilpng.loadImage(info, null, session); + assertTrue(im instanceof ImageRendered); + } + + @Test + public void testGetTargetFlavor() { + assertEquals(ImageFlavor.RENDERED_IMAGE, ilpng.getTargetFlavor()); + } + +} diff --git a/src/test/java/org/apache/xmlgraphics/image/loader/impl/ImageLoaderRawCCITTFaxTestCase.java b/src/test/java/org/apache/xmlgraphics/image/loader/impl/ImageLoaderRawCCITTFaxTestCase.java new file mode 100644 index 0000000..b75c4a4 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/image/loader/impl/ImageLoaderRawCCITTFaxTestCase.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageLoaderRawCCITTFaxTestCase.java 1732019 2016-02-24 05:01:10Z gadams $ */ + +package org.apache.xmlgraphics.image.loader.impl; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +import org.apache.xmlgraphics.image.loader.ImageContext; +import org.apache.xmlgraphics.image.loader.ImageInfo; +import org.apache.xmlgraphics.image.loader.ImageSessionContext; +import org.apache.xmlgraphics.image.loader.ImageSize; +import org.apache.xmlgraphics.image.loader.MockImageContext; +import org.apache.xmlgraphics.image.loader.MockImageSessionContext; +import org.apache.xmlgraphics.util.MimeConstants; + +/** + * Test case for {@link ImageLoaderRawCCITTFax}. + */ +public class ImageLoaderRawCCITTFaxTestCase { + private ImageLoaderRawCCITTFax sut; + + @Test + public void testLoadImage() throws Exception { + ImageContext context = MockImageContext.newSafeInstance(); + ImageSessionContext session = new MockImageSessionContext(context); + // This image file is NOT a valid tif! It is the directory table ONLY. + ImageInfo info = new ImageInfo("dirOnly.tif", MimeConstants.MIME_TIFF); + ImageSize size = new ImageSize(); + // Size data can be retrieve by parsing the directory table in the TIFF + size.setSizeInPixels(1728, 2266); + size.setResolution(203, 192); + size.calcSizeFromPixels(); + info.setSize(size); + + sut = new ImageLoaderRawCCITTFax(); + ImageRawCCITTFax rawImage = (ImageRawCCITTFax) sut.loadImage(info, null, session); + assertEquals(2, rawImage.getCompression()); + } +} diff --git a/src/test/java/org/apache/xmlgraphics/image/loader/impl/ImageLoaderRawPNGTestCase.java b/src/test/java/org/apache/xmlgraphics/image/loader/impl/ImageLoaderRawPNGTestCase.java new file mode 100644 index 0000000..b38b3fd --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/image/loader/impl/ImageLoaderRawPNGTestCase.java @@ -0,0 +1,89 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageLoaderRawPNGTestCase.java 1732019 2016-02-24 05:01:10Z gadams $ */ + +package org.apache.xmlgraphics.image.loader.impl; + +import java.io.FileInputStream; +import java.io.IOException; + +import javax.imageio.ImageIO; +import javax.imageio.stream.ImageInputStream; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.apache.xmlgraphics.image.loader.Image; +import org.apache.xmlgraphics.image.loader.ImageContext; +import org.apache.xmlgraphics.image.loader.ImageException; +import org.apache.xmlgraphics.image.loader.ImageFlavor; +import org.apache.xmlgraphics.image.loader.ImageInfo; +import org.apache.xmlgraphics.image.loader.ImageSessionContext; +import org.apache.xmlgraphics.image.loader.ImageSource; +import org.apache.xmlgraphics.image.loader.MockImageContext; +import org.apache.xmlgraphics.image.loader.MockImageSessionContext; +import org.apache.xmlgraphics.util.MimeConstants; + +public class ImageLoaderRawPNGTestCase { + + private ImageLoaderRawPNG ilrpng = new ImageLoaderRawPNG(); + + @Test + public void testGetUsagePenalty() { + assertEquals(1000, ilrpng.getUsagePenalty()); + } + + @Test + public void testLoadImageBadMime() throws ImageException, IOException { + ImageContext context = MockImageContext.newSafeInstance(); + ImageSessionContext session = new MockImageSessionContext(context); + ImageInfo info = new ImageInfo("basn2c08.png", MimeConstants.MIME_JPEG); + try { + ImageRawPNG irpng = (ImageRawPNG) ilrpng.loadImage(info, null, session); + fail("An exception should have been thrown above"); + } catch (IllegalArgumentException e) { + // do nothing; this was expected + } + } + + @Test + public void testGetTargetFlavor() { + assertEquals(ImageFlavor.RAW_PNG, ilrpng.getTargetFlavor()); + } + + @Test + public void testLoadImageGoodMime() throws ImageException, IOException { + ImageContext context = MockImageContext.newSafeInstance(); + ImageSessionContext session = new MockImageSessionContext(context); + ImageInfo info = new ImageInfo("basn2c08.png", MimeConstants.MIME_PNG); + Image im = ilrpng.loadImage(info, null, session); + assertTrue(im instanceof ImageRawPNG); + } + + @Test + public void testPreloaderRawPNG() throws IOException, ImageException { + ImageInputStream iis = ImageIO.createImageInputStream(new FileInputStream("test/images/tbbn3p08.png")); + ImageContext context = MockImageContext.newSafeInstance(); + ImageInfo imageInfo = new PreloaderRawPNG().preloadImage(null, new ImageSource(iis, null, true), context); + assertEquals(imageInfo.getMimeType(), "image/png"); + assertEquals(imageInfo.getSize().getWidthPx(), 32); + } +} diff --git a/src/test/java/org/apache/xmlgraphics/image/loader/impl/PNGFileTestCase.java b/src/test/java/org/apache/xmlgraphics/image/loader/impl/PNGFileTestCase.java new file mode 100644 index 0000000..dbfc471 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/image/loader/impl/PNGFileTestCase.java @@ -0,0 +1,118 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PNGFileTestCase.java 1843559 2018-10-11 14:59:17Z ssteiner $ */ + +package org.apache.xmlgraphics.image.loader.impl; + +import java.awt.image.ColorModel; +import java.awt.image.ComponentColorModel; +import java.awt.image.IndexColorModel; +import java.io.IOException; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import org.apache.xmlgraphics.image.loader.ImageContext; +import org.apache.xmlgraphics.image.loader.ImageException; +import org.apache.xmlgraphics.image.loader.ImageInfo; +import org.apache.xmlgraphics.image.loader.ImageSessionContext; +import org.apache.xmlgraphics.image.loader.MockImageContext; +import org.apache.xmlgraphics.image.loader.MockImageSessionContext; +import org.apache.xmlgraphics.util.MimeConstants; + +public class PNGFileTestCase implements PNGConstants { + + @Test + public void testColorTypeTwoPNG() throws ImageException, IOException { + testColorTypePNG("basn2c08.png", PNG_COLOR_RGB); + } + + @Test + public void testColorTypeZeroPNG() throws ImageException, IOException { + testColorTypePNG("basn0g08.png", PNG_COLOR_GRAY); + } + + @Test + public void testColorTypeSixPNG() throws ImageException, IOException { + testColorTypePNG("basn6a08.png", PNG_COLOR_RGB_ALPHA); + } + + @Test + public void testColorTypeThreePNG() throws ImageException, IOException { + testColorTypePNG("basn3p08.png", PNG_COLOR_PALETTE); + } + + @Test + public void testColorTypeFourPNG() throws ImageException, IOException { + testColorTypePNG("basn4a08.png", PNG_COLOR_GRAY_ALPHA); + } + + @Test + public void testTransparentPNG() throws ImageException, IOException { + testColorTypePNG("tbbn3p08.png", PNG_COLOR_PALETTE, true); + testColorTypePNG("tbrn2c08.png", PNG_COLOR_RGB, true); + } + + @Test + public void testCorruptPNG() { + ImageContext context = MockImageContext.newSafeInstance(); + ImageSessionContext session = new MockImageSessionContext(context); + ImageInfo info = new ImageInfo("corrupt-image.png", MimeConstants.MIME_PNG); + ImageLoaderRawPNG ilrpng = new ImageLoaderRawPNG(); + String exception = ""; + try { + ilrpng.loadImage(info, null, session); + } catch (Exception e) { + exception = e.getCause().getMessage(); + } + assertEquals("PNG unknown critical chunk: IBLA", exception); + } + + private void testColorTypePNG(String imageName, int colorType) throws ImageException, IOException { + testColorTypePNG(imageName, colorType, false); + } + + private void testColorTypePNG(String imageName, int colorType, boolean isTransparent) + throws ImageException, IOException { + ImageContext context = MockImageContext.newSafeInstance(); + ImageSessionContext session = new MockImageSessionContext(context); + ImageInfo info = new ImageInfo(imageName, MimeConstants.MIME_PNG); + ImageLoaderRawPNG ilrpng = new ImageLoaderRawPNG(); + ImageRawPNG irpng = (ImageRawPNG) ilrpng.loadImage(info, null, session); + ColorModel cm = irpng.getColorModel(); + if (colorType == PNG_COLOR_PALETTE) { + assertTrue(cm instanceof IndexColorModel); + } else { + assertTrue(cm instanceof ComponentColorModel); + int numComponents = 3; + if (colorType == PNG_COLOR_GRAY) { + numComponents = 1; + } else if (colorType == PNG_COLOR_GRAY_ALPHA) { + numComponents = 2; + } else if (colorType == PNG_COLOR_RGB_ALPHA) { + numComponents = 4; + } + assertEquals(numComponents, cm.getNumComponents()); + } + if (isTransparent) { + assertTrue(irpng.isTransparent()); + } + } +} diff --git a/src/test/java/org/apache/xmlgraphics/image/loader/impl/PreloaderJPEGTestCase.java b/src/test/java/org/apache/xmlgraphics/image/loader/impl/PreloaderJPEGTestCase.java new file mode 100644 index 0000000..925339d --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/image/loader/impl/PreloaderJPEGTestCase.java @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PreloaderJPEGTestCase.java 1732019 2016-02-24 05:01:10Z gadams $ */ + +package org.apache.xmlgraphics.image.loader.impl; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +import javax.imageio.stream.MemoryCacheImageInputStream; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.apache.xmlgraphics.image.loader.ImageContext; +import org.apache.xmlgraphics.image.loader.ImageException; +import org.apache.xmlgraphics.image.loader.ImageInfo; +import org.apache.xmlgraphics.image.loader.ImageSize; +import org.apache.xmlgraphics.image.loader.ImageSource; + +public class PreloaderJPEGTestCase { + + @Test + public void testAPP1Segment() throws IOException, ImageException { + + // example from http://www.media.mit.edu/pia/Research/deepview/exif.html (adapted and expanded) + // the bytes below have three markers: 0xFFD8 (SOI), 0xFFE1 (APP1) and 0xFFC0 (SOF0); this all + // it is needed to get the image size and resolution for this test + byte[] jpegBytes = {(byte) 0xFF, (byte) 0xD8, (byte) 0xFF, (byte) 0xE1, (byte) 0x00, (byte) 0x42, + (byte) 0x45, (byte) 0x78, (byte) 0x69, (byte) 0x66, (byte) 0x00, (byte) 0x00, (byte) 0x49, + (byte) 0x49, (byte) 0x2A, (byte) 0x00, (byte) 0x08, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x03, (byte) 0x00, (byte) 0x1A, (byte) 0x01, (byte) 0x05, (byte) 0x00, (byte) 0x01, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x32, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x28, (byte) 0x01, (byte) 0x03, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x02, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x69, (byte) 0x87, + (byte) 0x04, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x11, + (byte) 0x02, (byte) 0x00, (byte) 0x00, (byte) 0x40, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0xC0, (byte) 0xC6, (byte) 0x2D, (byte) 0x00, (byte) 0x10, (byte) 0x27, (byte) 0x00, + (byte) 0x00, (byte) 0xFF, (byte) 0xC0, (byte) 0x00, (byte) 0x14, (byte) 0x00, (byte) 0x0D, + (byte) 0xB4, (byte) 0x09, (byte) 0xB0, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00}; + InputStream jpegInputStream = new ByteArrayInputStream(jpegBytes); + MemoryCacheImageInputStream jpeg = new MemoryCacheImageInputStream(jpegInputStream); + + String uri = "image.jpg"; + ImageSource imageSource = mock(ImageSource.class); + ImageContext context = mock(ImageContext.class); + when(imageSource.getImageInputStream()).thenReturn(jpeg); + + PreloaderJPEG preloaderJPEG = new PreloaderJPEG(); + ImageInfo imageInfo = preloaderJPEG.preloadImage(uri, imageSource, context); + ImageSize imageSize = imageInfo.getSize(); + + double expectedDPI = 300.0; + assertEquals(expectedDPI, imageSize.getDpiHorizontal(), 0.01); + } + +} diff --git a/src/test/java/org/apache/xmlgraphics/image/loader/impl/gray-vs-cmyk-profile.png b/src/test/java/org/apache/xmlgraphics/image/loader/impl/gray-vs-cmyk-profile.png new file mode 100644 index 0000000..707e46a Binary files /dev/null and b/src/test/java/org/apache/xmlgraphics/image/loader/impl/gray-vs-cmyk-profile.png differ diff --git a/src/test/java/org/apache/xmlgraphics/image/loader/mocks/MockImageLoaderFactoryTIFF.java b/src/test/java/org/apache/xmlgraphics/image/loader/mocks/MockImageLoaderFactoryTIFF.java new file mode 100644 index 0000000..6ff6261 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/image/loader/mocks/MockImageLoaderFactoryTIFF.java @@ -0,0 +1,108 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: MockImageLoaderFactoryTIFF.java 1365650 2012-07-25 15:59:30Z mehdi $ */ + +package org.apache.xmlgraphics.image.loader.mocks; + +import java.io.IOException; +import java.util.Map; + +import static org.junit.Assert.assertTrue; + +import org.apache.xmlgraphics.image.loader.Image; +import org.apache.xmlgraphics.image.loader.ImageException; +import org.apache.xmlgraphics.image.loader.ImageFlavor; +import org.apache.xmlgraphics.image.loader.ImageInfo; +import org.apache.xmlgraphics.image.loader.ImageSessionContext; +import org.apache.xmlgraphics.image.loader.impl.AbstractImageLoaderFactory; +import org.apache.xmlgraphics.image.loader.spi.ImageLoader; +import org.apache.xmlgraphics.util.MimeConstants; + +/** + * Mock implementation posing as a TIFF-compatible loader. + */ +public class MockImageLoaderFactoryTIFF extends AbstractImageLoaderFactory { + + /** {@inheritDoc} */ + public ImageFlavor[] getSupportedFlavors(String mime) { + return new ImageFlavor[] {ImageFlavor.BUFFERED_IMAGE, ImageFlavor.RENDERED_IMAGE}; + } + + /** {@inheritDoc} */ + public String[] getSupportedMIMETypes() { + return new String[] {MimeConstants.MIME_TIFF}; + } + + private void checkSuppportedFlavor(String mime, ImageFlavor flavor) { + ImageFlavor[] flavors = getSupportedFlavors(mime); + boolean found = false; + for (int i = 0; i < flavors.length; i++) { + if (flavors[i].equals(flavor)) { + found = true; + break; + } + } + assertTrue(found); + } + + /** {@inheritDoc} */ + public boolean isAvailable() { + return true; + } + + /** {@inheritDoc} */ + public boolean isSupported(ImageInfo imageInfo) { + return MimeConstants.MIME_TIFF.equals(imageInfo.getMimeType()); + } + + /** {@inheritDoc} */ + public ImageLoader newImageLoader(ImageFlavor targetFlavor) { + checkSuppportedFlavor(MimeConstants.MIME_TIFF, targetFlavor); + return new ImageLoaderImpl(targetFlavor); + } + + /** Mock image loader implementation. */ + private static class ImageLoaderImpl implements ImageLoader { + + private ImageFlavor flavor; + + public ImageLoaderImpl(ImageFlavor flavor) { + this.flavor = flavor; + } + + public ImageFlavor getTargetFlavor() { + return flavor; + } + + public int getUsagePenalty() { + return 0; + } + + public Image loadImage(ImageInfo info, Map hints, ImageSessionContext session) + throws ImageException, IOException { + throw new UnsupportedOperationException("not implemented"); + } + + public Image loadImage(ImageInfo info, ImageSessionContext session) throws ImageException, + IOException { + throw new UnsupportedOperationException("not implemented"); + } + + } + +} diff --git a/src/test/java/org/apache/xmlgraphics/image/writer/ImageIOCheckUtility.java b/src/test/java/org/apache/xmlgraphics/image/writer/ImageIOCheckUtility.java new file mode 100644 index 0000000..03b8351 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/image/writer/ImageIOCheckUtility.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageIOCheckUtility.java 1732019 2016-02-24 05:01:10Z gadams $ */ + +package org.apache.xmlgraphics.image.writer; + +import java.util.Iterator; + +import javax.imageio.ImageIO; + +public final class ImageIOCheckUtility { + + private ImageIOCheckUtility() { + } + + /** + * Determines whether the JAI ImageIO library is present to run tests + * @return Returns true if the library is present. + */ + public static boolean isSunTIFFImageWriterAvailable() { + Iterator tiffWriters + = ImageIO.getImageWritersByMIMEType("image/tiff"); + boolean found = false; + while (tiffWriters.hasNext()) { + javax.imageio.ImageWriter writer = tiffWriters.next(); + if ("com.sun.media.imageioimpl.plugins.tiff.TIFFImageWriter".equals( + writer.getClass().getName())) { + //JAI ImageIO implementation present + found = true; + break; + } + } + return found; + } +} diff --git a/src/test/java/org/apache/xmlgraphics/image/writer/ImageWriterRegistryTestCase.java b/src/test/java/org/apache/xmlgraphics/image/writer/ImageWriterRegistryTestCase.java new file mode 100644 index 0000000..364a953 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/image/writer/ImageWriterRegistryTestCase.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageWriterRegistryTestCase.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.image.writer; + +import java.awt.image.RenderedImage; +import java.io.IOException; +import java.io.OutputStream; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import org.apache.xmlgraphics.image.writer.imageio.ImageIOPNGImageWriter; + +/** + * Tests the {@link ImageWriterRegistry}. + */ +public class ImageWriterRegistryTestCase { + + @Test + public void testRegistry() throws Exception { + ImageWriterRegistry registry = new ImageWriterRegistry(); + + ImageWriter writer; + writer = registry.getWriterFor("image/something"); + assertNull(writer); + + writer = registry.getWriterFor("image/png"); + assertTrue(writer instanceof ImageIOPNGImageWriter); + + registry.register(new DummyPNGWriter()); + + ImageWriter dummy = registry.getWriterFor("image/png"); + assertEquals(DummyPNGWriter.class, dummy.getClass()); + + registry.register(new OtherPNGWriter(), 50); + + dummy = registry.getWriterFor("image/png"); + assertEquals(OtherPNGWriter.class, dummy.getClass()); + } + + private static class DummyPNGWriter extends AbstractImageWriter { + + public String getMIMEType() { + return "image/png"; + } + + public void writeImage(RenderedImage image, OutputStream out) throws IOException { + //nop + } + + public void writeImage(RenderedImage image, OutputStream out, ImageWriterParams params) + throws IOException { + //nop + } + } + + private static class OtherPNGWriter extends DummyPNGWriter { + } +} diff --git a/src/test/java/org/apache/xmlgraphics/image/writer/ResolutionTestCase.java b/src/test/java/org/apache/xmlgraphics/image/writer/ResolutionTestCase.java new file mode 100644 index 0000000..08b8249 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/image/writer/ResolutionTestCase.java @@ -0,0 +1,153 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ResolutionTestCase.java 1732019 2016-02-24 05:01:10Z gadams $ */ + +package org.apache.xmlgraphics.image.writer; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Iterator; + +import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.metadata.IIOMetadataFormatImpl; +import javax.imageio.stream.ImageInputStream; + +import org.junit.Test; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import org.apache.commons.io.IOUtils; + +import org.apache.xmlgraphics.util.UnitConv; + +public class ResolutionTestCase { + + @Test + public void testResolution() throws IOException { + File testDir = new File("./build/test/results"); + testDir.mkdirs(); + runChecksForFormat(testDir, "image/png", "png"); + runChecksForFormat(testDir, "image/jpeg", "jpg"); + if (ImageIOCheckUtility.isSunTIFFImageWriterAvailable()) { + runChecksForFormat(testDir, "image/tiff", "tif"); + } + } + + private void runChecksForFormat(File testDir, String format, String ext) + throws FileNotFoundException, IOException { + File testFile; + ImageWriterParams params1 = new ImageWriterParams(); + params1.setResolution(300); + + ImageWriterParams params2 = new ImageWriterParams(); + params2.setResolutionUnit(ResolutionUnit.CENTIMETER); + params2.setXResolution(118); //~300dpi + params2.setYResolution(79); //~200dpi + + testFile = new File(testDir, "ResolutionTest1." + ext); + writeImage(params1, testFile, format); + checkStdMetadata(testFile, UnitConv.IN2MM / 300f, UnitConv.IN2MM / 300f); + + testFile = new File(testDir, "ResolutionTest2." + ext); + writeImage(params2, testFile, format); + float multiplier = (!format.equals("image/tiff")) ? 10f : UnitConv.IN2MM; + checkStdMetadata(testFile, multiplier / 118f, multiplier / 79f); + } + + private void writeImage(ImageWriterParams params, File testFile, String mime) throws FileNotFoundException, + IOException { + BufferedImage img = createTestImage(); + ImageWriter writer = ImageWriterRegistry.getInstance().getWriterFor(mime); + assertNotNull(writer); + OutputStream out = new java.io.FileOutputStream(testFile); + try { + writer.writeImage(img, out, params); + } finally { + IOUtils.closeQuietly(out); + } + } + + private void checkStdMetadata(File testFile, float xRes, float yRes) throws IOException { + ImageInputStream in = ImageIO.createImageInputStream(testFile); + try { + Iterator iter = ImageIO.getImageReaders(in); + assertTrue(iter.hasNext()); + ImageReader reader = iter.next(); + reader.setInput(in); + IIOMetadata iiometa = reader.getImageMetadata(0); + assertNotNull(iiometa); + assertTrue(iiometa.isStandardMetadataFormatSupported()); + Element metanode = (Element)iiometa.getAsTree( + IIOMetadataFormatImpl.standardMetadataFormatName); + Element dim = getChild(metanode, "Dimension"); + float resHorz = 0.0f; + float resVert = 0.0f; + if (dim != null) { + Element child; + child = getChild(dim, "HorizontalPixelSize"); + if (child != null) { + resHorz = Float.parseFloat(child.getAttribute("value")); + } + child = getChild(dim, "VerticalPixelSize"); + if (child != null) { + resVert = Float.parseFloat(child.getAttribute("value")); + } + } + assertEquals(xRes, resHorz, 0.000001f); + assertEquals(yRes, resVert, 0.000001f); + } finally { + in.close(); + } + } + + private static Element getChild(Element el, String name) { + NodeList nodes = el.getElementsByTagName(name); + if (nodes.getLength() > 0) { + return (Element)nodes.item(0); + } else { + return null; + } + } + + private BufferedImage createTestImage() { + BufferedImage img = new BufferedImage(320, 240, BufferedImage.TYPE_BYTE_GRAY); + Graphics2D g2d = img.createGraphics(); + g2d.setBackground(Color.WHITE); + g2d.clearRect(0, 0, img.getWidth(), img.getHeight()); + g2d.setColor(Color.RED); + g2d.fillOval(120, 80, 40, 40); + g2d.setColor(Color.GREEN); + g2d.setStroke(new BasicStroke(3)); + g2d.drawRect(150, 120, 60, 50); + g2d.dispose(); + return img; + } + +} diff --git a/src/test/java/org/apache/xmlgraphics/image/writer/imageio/ImageIOTIFFImageWriterTestCase.java b/src/test/java/org/apache/xmlgraphics/image/writer/imageio/ImageIOTIFFImageWriterTestCase.java new file mode 100644 index 0000000..63dfba4 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/image/writer/imageio/ImageIOTIFFImageWriterTestCase.java @@ -0,0 +1,212 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageIOTIFFImageWriterTestCase.java 1833700 2018-06-18 10:08:45Z ssteiner $ */ + +package org.apache.xmlgraphics.image.writer.imageio; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.geom.Ellipse2D; +import java.awt.image.BufferedImage; +import java.io.IOException; + +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.metadata.IIOMetadataNode; + +import org.junit.Assert; +import org.junit.Test; + +import org.w3c.dom.Node; + +import org.apache.commons.io.output.ByteArrayOutputStream; + +import org.apache.xmlgraphics.image.loader.ImageSize; +import org.apache.xmlgraphics.image.writer.Endianness; +import org.apache.xmlgraphics.image.writer.ImageIOCheckUtility; +import org.apache.xmlgraphics.image.writer.ImageWriter; +import org.apache.xmlgraphics.image.writer.ImageWriterParams; +import org.apache.xmlgraphics.image.writer.MultiImageWriter; +import org.apache.xmlgraphics.image.writer.ResolutionUnit; +import org.apache.xmlgraphics.util.UnitConv; + +/** + * Tests for {@link ImageIOTIFFImageWriter}. + */ +public class ImageIOTIFFImageWriterTestCase { + + /** + * Checks endianness when writing multi-page TIFF. + * @throws Exception if an error occurs + */ + @Test + public void testEndianess() throws Exception { + runEndiannessTest(new TestImageWriter(), 300); + runEndiannessTest(new TestMultiImageWriter(), 96); + } + + private void runEndiannessTest(ImageWriterHelper imageWriterHelper, int resolution) throws Exception { + if (!ImageIOCheckUtility.isSunTIFFImageWriterAvailable()) { + System.out.println("Skipping endianness test for ImageIO-based TIFF writer" + + " because JAI ImageIO Tools is not available!"); + return; //No JAI ImageIO TIFF codec available: skipping test + } + + BufferedImage image = createTestImage(resolution); + + ImageWriterParams params = new ImageWriterParams(); + params.setCompressionMethod("CCITT T.6"); + params.setResolution(resolution); + params.setSingleStrip(true); + params.setEndianness(Endianness.LITTLE_ENDIAN); + params.setResolutionUnit(ResolutionUnit.INCH); + + ImageWriter writer = new ImageIOTIFFImageWriter(); + imageWriterHelper.createImageWriter(writer); + imageWriterHelper.writeImage(image, params); + byte[] tiffData = imageWriterHelper.getByteArrayOutput().toByteArray(); + Assert.assertEquals('I', tiffData[0]); + Assert.assertEquals('I', tiffData[1]); + + //Switch to big endian + params.setEndianness(Endianness.BIG_ENDIAN); + imageWriterHelper.createImageWriter(writer); + imageWriterHelper.writeImage(image, params); + tiffData = imageWriterHelper.getByteArrayOutput().toByteArray(); + Assert.assertEquals('M', tiffData[0]); + Assert.assertEquals('M', tiffData[1]); + + //Test with no params (TIFF codec defaults to big endian) + imageWriterHelper.createImageWriter(writer); + imageWriterHelper.writeImageNoParams(image); + if (imageWriterHelper.getByteArrayOutput().size() > 0) { + tiffData = imageWriterHelper.getByteArrayOutput().toByteArray(); + Assert.assertEquals('M', tiffData[0]); + Assert.assertEquals('M', tiffData[1]); + } + } + + private BufferedImage createTestImage(int dpi) { + ImageSize size = new ImageSize(); + size.setSizeInMillipoints((int)UnitConv.mm2mpt(210), (int)UnitConv.mm2mpt(297)); + size.setResolution(dpi); + size.calcPixelsFromSize(); + int w = size.getWidthPx(); + int h = size.getHeightPx(); + + BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_BYTE_BINARY); + Graphics2D g2d = (Graphics2D)image.getGraphics(); + g2d.setBackground(Color.WHITE); + g2d.setColor(Color.BLACK); + g2d.clearRect(0, 0, image.getWidth(), image.getHeight()); + g2d.setFont(new Font("sans-serif", Font.PLAIN, 15)); + g2d.drawString("This is some test text!", 20, 50); + g2d.setStroke(new BasicStroke(2)); + g2d.draw(new Ellipse2D.Float(200, 200, 50, 50)); + g2d.dispose(); + + return image; + } + + private interface ImageWriterHelper { + void createImageWriter(ImageWriter imageWriter) throws IOException; + void writeImage(BufferedImage image, ImageWriterParams params) throws IOException; + void writeImageNoParams(BufferedImage image) throws IOException; + ByteArrayOutputStream getByteArrayOutput(); + } + + private static class TestImageWriter implements ImageWriterHelper { + private ImageWriter writer; + private ByteArrayOutputStream baout; + + public void createImageWriter(ImageWriter imageWriter) throws IOException { + baout = new ByteArrayOutputStream(); + writer = new ImageIOTIFFImageWriter(); + } + + public void writeImage(BufferedImage image, ImageWriterParams params) + throws IOException { + writer.writeImage(image, baout, params); + } + + public void writeImageNoParams(BufferedImage image) throws IOException { + writer.writeImage(image, baout); + } + + public ByteArrayOutputStream getByteArrayOutput() { + return baout; + } + } + + private static class TestMultiImageWriter implements ImageWriterHelper { + private MultiImageWriter writer; + private ByteArrayOutputStream baout; + + public void createImageWriter(ImageWriter imageWriter) + throws IOException { + baout = new ByteArrayOutputStream(); + writer = imageWriter.createMultiImageWriter(baout); + } + + public void writeImage(BufferedImage image, ImageWriterParams params) + throws IOException { + //Writer the same image twice (producing 2 pages) + writer.writeImage(image, params); + writer.writeImage(image, params); + writer.close(); + } + + public void writeImageNoParams(BufferedImage image) throws IOException { + //Not needed on a multi-image writer + } + + public ByteArrayOutputStream getByteArrayOutput() { + return baout; + } + } + + @Test + public void testNewMetadataFormat() { + ImageWriterParams params = new ImageWriterParams(); + params.setResolution(92); + MyIIOMetadata metadata = new MyIIOMetadata(); + new ImageIOTIFFImageWriter().updateMetadata(null, metadata, params); + Assert.assertEquals(metadata.mergeNode, "javax_imageio_tiff_image_1.0"); + } + + static class MyIIOMetadata extends IIOMetadata { + String mergeNode; + MyIIOMetadata() { + super(true, "javax_imageio_tiff_image_1.0", null, null, null); + } + public boolean isReadOnly() { + return false; + } + public Node getAsTree(String formatName) { + IIOMetadataNode node = new IIOMetadataNode(); + node.appendChild(new IIOMetadataNode("Dimension")); + return node; + } + public void mergeTree(String formatName, Node root) { + mergeNode = root.getNodeName(); + } + public void reset() { + } + }; +} diff --git a/src/test/java/org/apache/xmlgraphics/io/TempResourceURIGeneratorTestCase.java b/src/test/java/org/apache/xmlgraphics/io/TempResourceURIGeneratorTestCase.java new file mode 100644 index 0000000..5c472e5 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/io/TempResourceURIGeneratorTestCase.java @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.xmlgraphics.io; + +import java.net.URI; +import java.util.HashSet; +import java.util.Set; +import java.util.regex.Pattern; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; + +public class TempResourceURIGeneratorTestCase { + + private TempResourceURIGenerator sut = new TempResourceURIGenerator("test"); + + @Test + public void testGenerate() { + URI first = sut.generate(); + URI second = sut.generate(); + Pattern regex = Pattern.compile("tmp:///test.*"); + assertTrue(regex.matcher(first.toASCIIString()).matches()); + assertTrue(regex.matcher(second.toASCIIString()).matches()); + assertNotSame(first, second); + + // Test that they are unique over a large number of calls to generate() + Set uniqueSet = new HashSet(); + int numberOfTests = 1000; + for (int i = 0; i < numberOfTests; i++) { + uniqueSet.add(sut.generate()); + } + assertEquals(numberOfTests, uniqueSet.size()); + } + + @Test + public void testIsTemURI() { + assertTrue(testTempURI("tmp:///test")); + assertTrue(testTempURI("tmp://test")); + assertTrue(testTempURI("tmp:/test")); + assertTrue(testTempURI("tmp:test")); + + assertFalse(testTempURI("tmp/test")); + assertFalse(testTempURI("temp:///test")); + } + + private boolean testTempURI(String uriString) { + return TempResourceURIGenerator.isTempURI(URI.create(uriString)); + } +} diff --git a/src/test/java/org/apache/xmlgraphics/io/URIResolverAdapterTestCase.java b/src/test/java/org/apache/xmlgraphics/io/URIResolverAdapterTestCase.java new file mode 100644 index 0000000..e9c3094 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/io/URIResolverAdapterTestCase.java @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.xmlgraphics.io; + +import java.io.IOException; +import java.io.InputStream; +import java.io.StringWriter; +import java.net.URI; +import java.net.URL; + +import javax.xml.transform.Source; +import javax.xml.transform.TransformerException; +import javax.xml.transform.sax.SAXSource; + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +import org.apache.commons.io.IOUtils; +import org.apache.xml.resolver.tools.CatalogResolver; + +public class URIResolverAdapterTestCase { + + private final URI textFileURI = URI.create("test:catalog:resolver:testResource.txt"); + private final URI httpURL = URI.create("test:http:protocol:test.html"); + private final String pathOfTestFile = "src/test/resources/org/apache/xmlgraphics/io/test-catalog.xml"; + + @Before + public void setUp() { + System.setProperty("xml.catalog.files", pathOfTestFile); + } + + @Test + @Ignore("Literally no idea why this doesn't work... Gonna look at the catalog resolver source") + public void testCatalogResolver() throws TransformerException, IOException { + CatalogResolver catalogResolver = new CatalogResolver(); + Source src = catalogResolver.resolve(textFileURI.toASCIIString(), null); + if (src instanceof SAXSource) { + testInputStream(new URL(src.getSystemId()).openStream()); + } + } + + @Test + @Ignore("Literally no idea why this doesn't work... Gonna look at the catalog resolver source") + public void testCatalogResolverInAdapter() throws IOException { + ResourceResolver resourceResolver = new URIResolverAdapter(new CatalogResolver()); + testInputStream(resourceResolver.getResource(textFileURI)); + } + + private void testInputStream(InputStream stream) throws IOException { + StringWriter writer = new StringWriter(); + IOUtils.copy(stream, writer); + assertEquals("This is a text file used to test the CatalogResolver\n", writer.toString()); + } + + @Test + public void testHttpProtocol() throws TransformerException { + String url = "http://svn.apache.org/repos/asf/xmlgraphics/fop/trunk/test/resources/images/test.html"; + CatalogResolver catalogResolver = new CatalogResolver(); + Source src = catalogResolver.resolve(httpURL.toASCIIString(), null); + assertEquals(url, src.getSystemId()); + } +} diff --git a/src/test/java/org/apache/xmlgraphics/io/XmlSourceUtilTestCase.java b/src/test/java/org/apache/xmlgraphics/io/XmlSourceUtilTestCase.java new file mode 100644 index 0000000..52bc3aa --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/io/XmlSourceUtilTestCase.java @@ -0,0 +1,237 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.xmlgraphics.io; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.io.StringWriter; + +import javax.imageio.stream.ImageInputStream; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.Source; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.sax.SAXSource; +import javax.xml.transform.stream.StreamSource; + +import org.junit.Before; +import org.junit.Test; +import org.xml.sax.InputSource; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.apache.commons.io.IOUtils; + +import org.apache.xmlgraphics.image.loader.ImageSource; + +import static org.apache.xmlgraphics.io.XmlSourceUtil.closeQuietly; +import static org.apache.xmlgraphics.io.XmlSourceUtil.getInputStream; +import static org.apache.xmlgraphics.io.XmlSourceUtil.hasInputStream; +import static org.apache.xmlgraphics.io.XmlSourceUtil.hasReader; +import static org.apache.xmlgraphics.io.XmlSourceUtil.needInputStream; +import static org.apache.xmlgraphics.io.XmlSourceUtil.removeStreams; + +public class XmlSourceUtilTestCase { + + private StreamSource streamSource; + private SAXSource saxSource; + private InputSource inputSource; + private ImageSource imgSource; + private ImageInputStream imgInStream; + private DOMSource domSource; + private StringWriter writer; + private InputStream testStream; + private Reader reader; + + @Before + public void setUp() throws IOException, ParserConfigurationException { + testStream = mock(InputStream.class); + reader = mock(Reader.class); + + streamSource = mock(StreamSource.class); + when(streamSource.getInputStream()).thenReturn(testStream); + when(streamSource.getReader()).thenReturn(reader); + + saxSource = mock(SAXSource.class); + inputSource = mock(InputSource.class); + when(saxSource.getInputSource()).thenReturn(inputSource); + when(inputSource.getByteStream()).thenReturn(testStream); + when(inputSource.getCharacterStream()).thenReturn(reader); + + imgSource = mock(ImageSource.class); + imgInStream = mock(ImageInputStream.class); + when(imgSource.getImageInputStream()).thenReturn(imgInStream); + + + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + DocumentBuilder db = dbf.newDocumentBuilder(); + domSource = new DOMSource(db.newDocument().createElement("test")); + InputStream inStream = XmlSourceUtil.getInputStream(domSource); + writer = new StringWriter(); + IOUtils.copy(inStream, writer); + } + + @Test + public void testGetInputStream() throws ParserConfigurationException, IOException { + getInputStream(streamSource); + verify(streamSource).getInputStream(); + + getInputStream(saxSource); + verify(inputSource).getByteStream(); + + getInputStream(imgSource); + verify(imgSource).getImageInputStream(); + + assertEquals("", writer.toString()); + + // Test negative case + Source src = mock(Source.class); + assertNull(getInputStream(src)); + + getInputStream(null); + } + + @Test + public void testNeedInputStream() throws IOException, ParserConfigurationException { + assertEquals(testStream, needInputStream(streamSource)); + + assertEquals(testStream, needInputStream(saxSource)); + + needInputStream(imgSource); + verify(imgSource).getImageInputStream(); + + assertEquals("", writer.toString()); + } + + @Test(expected = IllegalArgumentException.class) + public void testNeedInputStreamFailureCaseSource() { + Source src = mock(Source.class); + needInputStream(src); + } + + @Test(expected = IllegalArgumentException.class) + public void testNeedInputStreamFailureCaseStreamSource() { + needInputStream(mock(StreamSource.class)); + } + + @Test(expected = IllegalArgumentException.class) + public void testNeedInputStreamFailureCaseSAXSource() { + needInputStream(mock(SAXSource.class)); + } + + public void testNeedInputStreamFailureCaseDOMSource() throws IOException { + InputStream inStream = needInputStream(new DOMSource()); + StringWriter writer = new StringWriter(); + IOUtils.copy(inStream, writer); + assertEquals("", writer.toString()); + } + + @Test(expected = AssertionError.class) + public void testNeedInputStreamFailureCaseStreamImage() { + needInputStream(mock(ImageSource.class)); + } + + @Test(expected = IllegalArgumentException.class) + public void testNeedInputStreamFailureCaseNullArg() { + needInputStream(null); + } + + @Test + public void testHasReader() { + assertTrue(hasReader(streamSource)); + + assertTrue(hasReader(saxSource)); + + when(streamSource.getReader()).thenReturn(null); + when(inputSource.getCharacterStream()).thenReturn(null); + + assertFalse(hasReader(streamSource)); + assertFalse(hasReader(saxSource)); + assertFalse(hasReader(imgSource)); + assertFalse(hasReader(domSource)); + + hasReader(null); + } + + @Test + public void testRemoveStreams() { + removeStreams(streamSource); + verify(streamSource).setInputStream(null); + verify(streamSource).setReader(null); + + removeStreams(imgSource); + verify(imgSource).setImageInputStream(null); + + removeStreams(saxSource); + verify(inputSource).setByteStream(null); + verify(inputSource).setCharacterStream(null); + + removeStreams(null); + } + + @Test + public void testCloseQuietlyStreamSource() throws IOException { + closeQuietly(streamSource); + verify(reader).close(); + verify(streamSource).setInputStream(null); + verify(streamSource).setReader(null); + } + + @Test + public void testCloseQuietlySaxSource() throws IOException { + closeQuietly(saxSource); + verify(testStream).close(); + verify(reader).close(); + verify(inputSource).setByteStream(null); + verify(inputSource).setCharacterStream(null); + } + + @Test + public void testCloseQuietlyImageSource() throws IOException { + closeQuietly(imgSource); + verify(imgInStream).close(); + verify(imgSource).setImageInputStream(null); + } + + @Test + public void testCloseQuietlyNull() { + XmlSourceUtil.closeQuietly(null); + } + + @Test + public void testHasInputStream() { + assertTrue(hasInputStream(streamSource)); + assertTrue(hasInputStream(saxSource)); + assertTrue(hasInputStream(imgSource)); + assertTrue(hasInputStream(domSource)); + + assertFalse(hasInputStream(mock(StreamSource.class))); + assertFalse(hasInputStream(mock(SAXSource.class))); + // Can't do the ImageSource test because of an assert, do we want that assert there? + // assertFalse(hasInputStream(mock(ImageSource.class))); + assertFalse(hasInputStream(mock(StreamSource.class))); + } +} diff --git a/src/test/java/org/apache/xmlgraphics/java2d/GraphicContextTestCase.java b/src/test/java/org/apache/xmlgraphics/java2d/GraphicContextTestCase.java new file mode 100644 index 0000000..68dfd82 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/java2d/GraphicContextTestCase.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: GraphicContextTestCase.java 1732019 2016-02-24 05:01:10Z gadams $ */ + +package org.apache.xmlgraphics.java2d; + +import java.awt.Color; +import java.awt.GradientPaint; +import java.awt.geom.Point2D; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class GraphicContextTestCase { + + @Test + public void testSetPaint() { + GraphicContext gc = new GraphicContext(); + Color red = Color.RED; + gc.setPaint(red); + assertEquals(red, gc.getColor()); + Point2D start = new Point2D.Float(0, 0); + Point2D end = new Point2D.Float(50, 50); + GradientPaint gp = new GradientPaint(start, Color.RED, end, Color.BLUE); + gc.setPaint(gp); + assertEquals(Color.BLACK, gc.getColor()); + } +} diff --git a/src/test/java/org/apache/xmlgraphics/java2d/GraphicsConfigurationWithTransparencyTestCase.java b/src/test/java/org/apache/xmlgraphics/java2d/GraphicsConfigurationWithTransparencyTestCase.java new file mode 100644 index 0000000..032a9e7 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/java2d/GraphicsConfigurationWithTransparencyTestCase.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.xmlgraphics.java2d; + +import java.awt.GraphicsConfiguration; +import java.awt.Transparency; +import java.awt.geom.AffineTransform; +import java.awt.image.BufferedImage; +import java.awt.image.ColorModel; + +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * Test case for {@link GraphicsConfigurationWithTransparency}. + */ +public class GraphicsConfigurationWithTransparencyTestCase { + + private ColorModel transparencyColorModel; + private ColorModel nonTransparencyColorModel; + private GraphicsConfiguration sut; + + @Before + public void setUp() { + sut = new GraphicsConfigurationWithTransparency(); + transparencyColorModel = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB).getColorModel(); + nonTransparencyColorModel = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB).getColorModel(); + } + + @Test + public void testGetNormalizationTransformation() { + AffineTransform transform = new AffineTransform(2, 0, 0, 2, 0, 0); + assertEquals(transform, sut.getNormalizingTransform()); + } + + void testImage(int width, int height, boolean hasTransparency, BufferedImage image) { + assertEquals(width, image.getWidth()); + assertEquals(height, image.getHeight()); + assertEquals(hasTransparency, image.getColorModel().hasAlpha()); + } + + @Test + public void testCreateCompatibleImage() { + testImage(1, 2, true, sut.createCompatibleImage(1, 2, Transparency.TRANSLUCENT)); + testImage(100, 90, true, sut.createCompatibleImage(100, 90, Transparency.TRANSLUCENT)); + testImage(1, 2, false, sut.createCompatibleImage(1, 2, Transparency.OPAQUE)); + testImage(1010, 2020, false, sut.createCompatibleImage(1010, 2020, Transparency.OPAQUE)); + + // test the 2 argument overriden method + testImage(1, 2, true, sut.createCompatibleImage(1, 2)); + testImage(1010, 2020, true, sut.createCompatibleImage(1010, 2020)); + } + + @Test + public void testGetColorModel() { + assertEquals(transparencyColorModel, sut.getColorModel()); + + assertEquals(transparencyColorModel, sut.getColorModel(Transparency.TRANSLUCENT)); + assertEquals(nonTransparencyColorModel, sut.getColorModel(Transparency.OPAQUE)); + } +} diff --git a/src/test/java/org/apache/xmlgraphics/java2d/GraphicsConfigurationWithoutTransparencyTestCase.java b/src/test/java/org/apache/xmlgraphics/java2d/GraphicsConfigurationWithoutTransparencyTestCase.java new file mode 100644 index 0000000..80b9803 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/java2d/GraphicsConfigurationWithoutTransparencyTestCase.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.xmlgraphics.java2d; + +import java.awt.GraphicsConfiguration; +import java.awt.Transparency; +import java.awt.geom.AffineTransform; +import java.awt.image.BufferedImage; +import java.awt.image.ColorModel; + +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class GraphicsConfigurationWithoutTransparencyTestCase { + + private ColorModel nonTransparencyColorModel; + private GraphicsConfiguration sut; + + @Before + public void setUp() { + sut = new GraphicsConfigurationWithoutTransparency(); + nonTransparencyColorModel = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB).getColorModel(); + } + + @Test + public void testGetNormalizationTransformation() { + AffineTransform transform = new AffineTransform(2, 0, 0, 2, 0, 0); + assertEquals(transform, sut.getNormalizingTransform()); + } + + void testImage(int width, int height, boolean hasTransparency, BufferedImage image) { + assertEquals(width, image.getWidth()); + assertEquals(height, image.getHeight()); + assertEquals(hasTransparency, image.getColorModel().hasAlpha()); + } + + @Test + public void testCreateCompatibleImage() { + testImage(1, 2, false, sut.createCompatibleImage(1, 2, Transparency.TRANSLUCENT)); + testImage(100, 90, false, sut.createCompatibleImage(100, 90, Transparency.TRANSLUCENT)); + testImage(1, 2, false, sut.createCompatibleImage(1, 2, Transparency.OPAQUE)); + testImage(1010, 2020, false, sut.createCompatibleImage(1010, 2020, Transparency.OPAQUE)); + + // test the 2 argument overriden method + testImage(1, 2, false, sut.createCompatibleImage(1, 2)); + testImage(1010, 2020, false, sut.createCompatibleImage(1010, 2020)); + } + + @Test + public void testGetColorModel() { + assertEquals(nonTransparencyColorModel, sut.getColorModel()); + + assertEquals(nonTransparencyColorModel, sut.getColorModel(Transparency.TRANSLUCENT)); + assertEquals(nonTransparencyColorModel, sut.getColorModel(Transparency.OPAQUE)); + } +} diff --git a/src/test/java/org/apache/xmlgraphics/java2d/color/ColorConverterTestCase.java b/src/test/java/org/apache/xmlgraphics/java2d/color/ColorConverterTestCase.java new file mode 100644 index 0000000..96bcb91 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/java2d/color/ColorConverterTestCase.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ColorConverterTestCase.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.java2d.color; + +import java.awt.Color; +import java.awt.color.ColorSpace; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class ColorConverterTestCase { + + @Test + public void testToGray() throws Exception { + ColorConverter converter = GrayScaleColorConverter.getInstance(); + Color rgb = new Color(255, 184, 0); + Color gray = converter.convert(rgb); + + ColorSpaceOrigin origin = ColorSpaces.getColorSpaceOrigin(gray.getColorSpace()); + assertEquals("#CMYK", origin.getProfileName()); + assertNull(origin.getProfileURI()); + assertEquals(ColorSpace.TYPE_CMYK, gray.getColorSpace().getType()); + float[] comps = gray.getColorComponents(null); + assertEquals(4, comps.length); + assertEquals(0.0f, comps[0], 0.1f); + assertEquals(0.0f, comps[1], 0.1f); + assertEquals(0.0f, comps[2], 0.1f); + assertEquals(0.273f, comps[3], 0.01f); + assertEquals(0xFFB9B9B9, gray.getRGB()); + } + +} diff --git a/src/test/java/org/apache/xmlgraphics/java2d/color/ColorWithAlternativesTestCase.java b/src/test/java/org/apache/xmlgraphics/java2d/color/ColorWithAlternativesTestCase.java new file mode 100644 index 0000000..43765f8 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/java2d/color/ColorWithAlternativesTestCase.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ColorWithAlternativesTestCase.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.java2d.color; + +import java.awt.Color; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * Tests the {@link ColorWithAlternatives} class. + */ +public class ColorWithAlternativesTestCase { + + @Test + public void testEquals() throws Exception { + Color col1 = new ColorWithAlternatives(255, 204, 0, null); + Color col2 = new Color(255, 204, 0); + + assertEquals(col1, col2); + assertEquals(col2, col1); + + CIELabColorSpace lab = ColorSpaces.getCIELabColorSpaceD50(); + Color postgelbLab = lab.toColor(83.25f, 16.45f, 96.89f, 1.0f); + col1 = new ColorWithAlternatives(255, 204, 0, new Color[] {postgelbLab}); + + //java.awt.Color tests on the sRGB value only + assertEquals(col1, col2); + assertEquals(col2, col1); + } + + @Test + public void testSameColor() throws Exception { + Color col1 = new ColorWithAlternatives(255, 204, 0, null); + Color col2 = new Color(255, 204, 0); + + //No alternatives. Only sRGB counts. + assertTrue(ColorUtil.isSameColor(col1, col2)); + + CIELabColorSpace lab = ColorSpaces.getCIELabColorSpaceD50(); + Color postgelbLab = lab.toColor(83.25f, 16.45f, 96.89f, 1.0f); + col1 = new ColorWithAlternatives(255, 204, 0, new Color[] {postgelbLab}); + + //Same sRGB value but one color with alternatives: + assertFalse(ColorUtil.isSameColor(col1, col2)); + + //Once the spotcolor naked and once as part of a color with alternatives + assertFalse(ColorUtil.isSameColor(postgelbLab, col1)); + + //sRGB values is calculated from Lab color and doesn't exactly match the selected fallback + assertFalse(postgelbLab.equals(col1)); + } +} diff --git a/src/test/java/org/apache/xmlgraphics/java2d/color/NamedColorTestCase.java b/src/test/java/org/apache/xmlgraphics/java2d/color/NamedColorTestCase.java new file mode 100644 index 0000000..a73277a --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/java2d/color/NamedColorTestCase.java @@ -0,0 +1,96 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: NamedColorTestCase.java 1829049 2018-04-13 09:37:06Z ssteiner $ */ + +package org.apache.xmlgraphics.java2d.color; + +import java.awt.Color; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +/** + * Tests named color spaces (and the CIE Lab color space implementation). + */ +public class NamedColorTestCase { + + private static final float POSTGELB_X = 0.6763079f; + private static final float POSTGELB_Y = 0.6263507f; + private static final float POSTGELB_Z = 0.04217565f; + + @Test + public void testNamedColorWithCIELab() { + CIELabColorSpace lab = ColorSpaces.getCIELabColorSpaceD50(); + + //CIE Lab definition of "Postgelb" (postal yellow) at D50 as defined by Swiss Post + //Convert to XYZ + float[] c1xyz = lab.toCIEXYZNative(83.25f, 16.45f, 96.89f); + //Verify XYZ values are OK + assertEquals(POSTGELB_X, c1xyz[0], 0.001f); + assertEquals(POSTGELB_Y, c1xyz[1], 0.001f); + assertEquals(POSTGELB_Z, c1xyz[2], 0.001f); + + //Build named color based on XYZ coordinates + NamedColorSpace ncs = new NamedColorSpace("Postgelb", c1xyz); + Color c1 = new Color(ncs, new float[] {1.0f}, 1.0f); + + assertEquals(ncs, c1.getColorSpace()); + float[] comp = c1.getColorComponents(null); + assertEquals(1, comp.length); + assertEquals(1.0f, comp[0], 0.001f); + float[] xyz = ncs.toCIEXYZ(new float[] {1.0f}); + for (int i = 0; i < 3; i++) { + assertEquals(c1xyz[i], xyz[i], 0.001f); + } + + //NOTE: Allowing for some fuzziness due to differences in XYZ->sRGB calculation between + //Java 1.5 and 6. + assertEquals(254, c1.getRed(), 1f); + assertEquals(195, c1.getGreen(), 2f); + assertEquals(0, c1.getBlue()); + } + + @Test + public void testEquals() { + NamedColorSpace ncs1 = new NamedColorSpace("Postgelb", + new float[] {POSTGELB_X, POSTGELB_Y, POSTGELB_Z}); + + NamedColorSpace ncs2 = new NamedColorSpace("Postgelb", + new float[] {POSTGELB_X, POSTGELB_Y, POSTGELB_Z}); + + assertEquals(ncs1, ncs2); + + //Construct the same NamedColorSpace via two different methods + CIELabColorSpace lab = ColorSpaces.getCIELabColorSpaceD50(); + Color postgelbLab = lab.toColor(83.25f, 16.45f, 96.89f, 1.0f); + float[] xyz = lab.toCIEXYZ(postgelbLab.getColorComponents(null)); + xyz[0] = POSTGELB_X; + xyz[1] = POSTGELB_Y; + xyz[2] = POSTGELB_Z; + ncs1 = new NamedColorSpace("Postgelb", postgelbLab); + ncs2 = new NamedColorSpace("Postgelb", xyz); + assertEquals(ncs1, ncs2); + + //Compare with a similar color coming from sRGB + Color rgb = new Color(255, 184, 0); + ncs2 = new NamedColorSpace("PostgelbFromRGB", rgb); + assertFalse(ncs1.equals(ncs2)); + } +} diff --git a/src/test/java/org/apache/xmlgraphics/java2d/color/profile/ColorProfileUtilTestCase.java b/src/test/java/org/apache/xmlgraphics/java2d/color/profile/ColorProfileUtilTestCase.java new file mode 100644 index 0000000..6d7e9c0 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/java2d/color/profile/ColorProfileUtilTestCase.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ColorProfileUtilTestCase.java 1732019 2016-02-24 05:01:10Z gadams $ */ + +package org.apache.xmlgraphics.java2d.color.profile; + +import java.awt.color.ColorSpace; +import java.awt.color.ICC_Profile; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import org.junit.Test; + +import static org.junit.Assert.assertTrue; + +public class ColorProfileUtilTestCase { + + @Test + public void testIsDefaultsRGB() throws IOException { + ICC_Profile profile = ICC_Profile.getInstance(ColorSpace.CS_sRGB); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + profile.write(baos); + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + ICC_Profile profileFromFile = ICC_Profile.getInstance(bais); + assertTrue(ColorProfileUtil.isDefaultsRGB(profileFromFile)); + } + +} diff --git a/src/test/java/org/apache/xmlgraphics/java2d/color/profile/NamedColorProfileParserTestCase.java b/src/test/java/org/apache/xmlgraphics/java2d/color/profile/NamedColorProfileParserTestCase.java new file mode 100644 index 0000000..2996c4c --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/java2d/color/profile/NamedColorProfileParserTestCase.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: NamedColorProfileParserTestCase.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.java2d.color.profile; + +import java.awt.color.ICC_Profile; +import java.io.InputStream; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.apache.commons.io.IOUtils; + +import org.apache.xmlgraphics.java2d.color.NamedColorSpace; +import org.apache.xmlgraphics.java2d.color.RenderingIntent; + +/** + * Tests the {@link NamedColorProfileParser}. + */ +public class NamedColorProfileParserTestCase { + + private static final String NCP_EXAMPLE_FILE = "ncp-example.icc"; + + @Test + public void testParser() throws Exception { + InputStream in = getClass().getResourceAsStream(NCP_EXAMPLE_FILE); + assertNotNull(NCP_EXAMPLE_FILE + " is missing!", in); + ICC_Profile iccProfile; + try { + iccProfile = ICC_Profile.getInstance(in); + } finally { + IOUtils.closeQuietly(in); + } + NamedColorProfileParser parser = new NamedColorProfileParser(); + NamedColorProfile ncp = parser.parseProfile(iccProfile); + assertEquals("Named Color Profile Example", ncp.getProfileName()); + assertEquals("The Apache Software Foundation", ncp.getCopyright()); + assertEquals(RenderingIntent.PERCEPTUAL, ncp.getRenderingIntent()); + NamedColorSpace[] namedColors = ncp.getNamedColors(); + assertEquals(2, namedColors.length); + NamedColorSpace ncs; + ncs = namedColors[0]; + assertEquals("Postgelb", ncs.getColorName()); + float[] xyz = ncs.getXYZ(); + assertEquals(0.6763079f, xyz[0], 0.01f); + assertEquals(0.6263507f, xyz[1], 0.01f); + assertEquals(0.04217565f, xyz[2], 0.01f); + + ncs = namedColors[1]; + assertEquals("MyRed", ncs.getColorName()); + } +} diff --git a/src/test/java/org/apache/xmlgraphics/java2d/ps/PSGraphics2DTestCase.java b/src/test/java/org/apache/xmlgraphics/java2d/ps/PSGraphics2DTestCase.java new file mode 100644 index 0000000..e32a6dc --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/java2d/ps/PSGraphics2DTestCase.java @@ -0,0 +1,179 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PSGraphics2DTestCase.java 1845492 2018-11-01 15:54:06Z ssteiner $ */ + +package org.apache.xmlgraphics.java2d.ps; + +import java.awt.AlphaComposite; +import java.awt.Rectangle; +import java.awt.Shape; +import java.awt.TexturePaint; +import java.awt.geom.AffineTransform; +import java.awt.geom.Line2D; +import java.awt.geom.Rectangle2D; +import java.awt.geom.RoundRectangle2D; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.apache.xmlgraphics.java2d.GraphicContext; +import org.apache.xmlgraphics.ps.PSGenerator; +import org.apache.xmlgraphics.ps.PSState; + +public class PSGraphics2DTestCase { + + private PSGenerator gen; + private PSGraphics2D gfx2d; + private final AffineTransform transform = new AffineTransform(1, 0, 0, -1, 0, 792); + + @Before + public void setup() { + gen = mock(PSGenerator.class); + createGraphics2D(); + PSState pState = new PSState(); + when(gen.getCurrentState()).thenReturn(pState); + } + + private void createGraphics2D() { + gfx2d = new PSGraphics2D(false, gen); + gfx2d.setGraphicContext(new GraphicContext()); + gfx2d.setTransform(transform); + } + + @Test + public void draw() throws IOException { + assertEquals(gfx2d.getTransform(), transform); + gfx2d.draw(new Rectangle(10, 10, 100, 100)); + verify(gen, times(1)).concatMatrix(transform); + } + + @Test + public void testShouldBeClipped() { + Shape line = new Line2D.Float(10, 10, 50, 50); + Shape clipArea = new Rectangle2D.Float(20, 20, 100, 100); + assertTrue(gfx2d.shouldBeClipped(clipArea, line)); + Shape rect = new Rectangle2D.Float(30, 30, 40, 40); + assertFalse(gfx2d.shouldBeClipped(clipArea, rect)); + } + + @Test + public void testFill() { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + PSGenerator gen = new PSGenerator(out); + PSGraphics2D p = new PSGraphics2D(false, gen); + p.setGraphicContext(new GraphicContext()); + p.fill(new RoundRectangle2D.Float()); + out.reset(); + + p.fill(new RoundRectangle2D.Float()); + assertEquals(out.toString(), + "GS\nN\n/f1943450110{0 0 M\n0 0 L\n0 0 0 0 0 0 C\n0 0 L\n0 0 0 0 0 0 C\n" + + "0 0 L\n0 0 0 0 0 0 C\n0 0 L\n0 0 0 0 0 0 C\ncp}def\nf1943450110\nf\nGR\n"); + out.reset(); + + BufferedImage img = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB); + p.drawImage(img, 0, 0, null); + + String res = "[1 0 0 1 0 0] CT\n" + + "GS\n" + + "0 0 translate\n" + + "%AXGBeginBitmap: java.awt.image.BufferedImage\n" + + "{{\n" + + "/RawData currentfile /ASCII85Decode filter def\n" + + "/Data RawData /FlateDecode filter def\n" + + "/DeviceRGB setcolorspace\n"; + + assertTrue(out.toString(), out.toString().startsWith("GS\n" + res)); + out.reset(); + + p.drawRenderedImage(img, new AffineTransform()); + assertTrue(out.toString(), out.toString().startsWith("GS\n[1 0 0 1 0 0] CT\n" + res)); + + out.reset(); + + p.writeClip(new RoundRectangle2D.Float()); + assertEquals(out.toString(), "N\n" + + "0 0 M\n" + + "0 0 L\n" + + "0 0 0 0 0 0 C\n" + + "0 0 L\n" + + "0 0 0 0 0 0 C\n" + + "0 0 L\n" + + "0 0 0 0 0 0 C\n" + + "0 0 L\n" + + "0 0 0 0 0 0 C\n" + + "cp\n" + + "clip\n"); + out.reset(); + + p.drawString("hi", 0f, 0f); + assertTrue(out.toString(), out.toString().startsWith("GS\nN\n/f")); + out.reset(); + + TexturePaint tp = new TexturePaint(img, new Rectangle()); + p.setPaint(tp); + p.fill(new Rectangle()); + assertTrue(out.toString().startsWith("GS\n<<\n/PatternType 1\n")); + + p.dispose(); + } + + @Test + public void testAcrobatDownsample() { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + PSGenerator gen = new PSGenerator(out); + PSGraphics2D p = new PSGraphics2D(false, gen); + p.setGraphicContext(new GraphicContext()); + BufferedImage img = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB); + TexturePaint tp = new TexturePaint(img, new Rectangle()); + p.setPaint(tp); + p.fill(new Rectangle()); + assertTrue(out.toString().contains("1 1 8 matrix\n{<\nffffff\n>} false 3 colorimage")); + out.reset(); + + gen.setAcrobatDownsample(true); + p.fill(new Rectangle()); + assertTrue(out.toString().contains("1 1 4 matrix\n{<\nfff\n>} false 3 colorimage")); + p.dispose(); + } + + @Test + public void testFillAlpha() { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + PSGenerator gen = new PSGenerator(out); + PSGraphics2D p = new PSGraphics2D(false, gen); + p.setGraphicContext(new GraphicContext()); + p.setComposite(AlphaComposite.getInstance(3, 0)); + p.fill(new Rectangle()); + assertEquals(out.toString(), ""); + p.setComposite(AlphaComposite.getInstance(3, 0.5f)); + p.fill(new Rectangle()); + assertTrue(out.toString().contains("\nN\n")); + } +} diff --git a/src/test/java/org/apache/xmlgraphics/ps/FormGeneratorTestCase.java b/src/test/java/org/apache/xmlgraphics/ps/FormGeneratorTestCase.java new file mode 100644 index 0000000..89ad8d4 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/ps/FormGeneratorTestCase.java @@ -0,0 +1,127 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.xmlgraphics.ps; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.color.ColorSpace; + +import java.awt.geom.Dimension2D; +import java.awt.image.BufferedImage; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.xmlgraphics.java2d.Dimension2DDouble; +import org.apache.xmlgraphics.java2d.color.NamedColorSpace; + + +public class FormGeneratorTestCase { + @Test + public void testGeneratePaintProc() throws IOException { + Dimension2D dimension = new Dimension2DDouble(300, 500); + BufferedImage im = new BufferedImage(100, 75, BufferedImage.TYPE_INT_ARGB); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ImageFormGenerator formImageGen = new ImageFormGenerator("form", "title", dimension, im, false); + PSGenerator gen = new PSGenerator(out); + formImageGen.generatePaintProc(gen); + String test = out.toString("UTF-8"); + + String expected = " form:Data 0 setfileposition\n" + + "[300 0 0 500 0 0] CT\n" + + "/DeviceRGB setcolorspace\n" + + "<<\n"; + Assert.assertTrue(test.contains(expected)); + Assert.assertTrue(test.contains(" /DataSource form:Data")); + Assert.assertTrue(test.contains(" /ImageMatrix [100 0 0 75 0 0]\n")); + Assert.assertTrue(test.contains(" /BitsPerComponent 8\n")); + Assert.assertTrue(test.contains(" /Height 75\n")); + Assert.assertTrue(test.contains(" /ImageType 1\n")); + Assert.assertTrue(test.contains(" /Decode [0 1 0 1 0 1]\n")); + Assert.assertTrue(test.contains(">> image\n")); + out.reset(); + im = null; + + Color c = Color.BLUE; + Dimension dimensionPX = new Dimension(200, 400); + ImageEncoder enco = ImageEncodingHelper.createRenderedImageEncoder(im); + ColorSpace cs = new NamedColorSpace("myColor", c); + formImageGen = new ImageFormGenerator("form", "title", dimension, dimensionPX, enco, cs, false); + gen = new PSGenerator(out); + gen.setPSLevel(2); + formImageGen.generatePaintProc(gen); + test = out.toString("UTF-8"); + expected = " userdict /i 0 put\n" + + "[300 0 0 500 0 0] CT\n" + + "/DeviceGray setcolorspace\n" + + "<<\n"; + Assert.assertTrue(test.contains(expected)); + Assert.assertTrue(test.contains(" /DataSource { form:Data i get /i i 1 add store } bind\n")); + Assert.assertTrue(test.contains(" /ImageMatrix [200 0 0 400 0 0]\n")); + Assert.assertTrue(test.contains(" /Height 400\n")); + Assert.assertTrue(test.contains(" /BitsPerComponent 8\n")); + Assert.assertTrue(test.contains(" /ImageType 1\n")); + Assert.assertTrue(test.contains(" /Decode [0 1]\n")); + Assert.assertTrue(test.contains(" /Width 200\n")); + Assert.assertTrue(test.contains(">> image\n")); + } + + @Test + public void testFlateDecodeCommand() throws IOException { + Dimension2D dimension = new Dimension2DDouble(300, 500); + BufferedImage im = new BufferedImage(100, 75, BufferedImage.TYPE_INT_ARGB); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ImageFormGenerator formImageGen = new ImageFormGenerator("form", "title", dimension, im, false); + PSGenerator gen = new PSGenerator(out); + formImageGen.generate(gen); + String test = out.toString("UTF-8"); + Assert.assertTrue(test.contains("/ASCII85Decode filter\n")); + //FlateDecode at DataSource so executed on page load rather than document load so viewer loads faster + Assert.assertTrue(test.contains("/DataSource form:Data /FlateDecode filter\n")); + } + + @Test + public void testAlphaImage() throws IOException { + Assert.assertEquals(buildPSImage(BufferedImage.TYPE_4BYTE_ABGR), buildPSImage(BufferedImage.TYPE_INT_RGB)); + } + + private String buildPSImage(int type) throws IOException { + Dimension2D dimension = new Dimension2DDouble(1, 1); + BufferedImage im = new BufferedImage(1, 1, type); + Graphics2D g = (Graphics2D) im.getGraphics(); + if (type == BufferedImage.TYPE_4BYTE_ABGR) { + g.setBackground(new Color(0, 0, 0, 0)); + } else { + g.setBackground(Color.white); + } + g.clearRect(0, 0, im.getWidth(), im.getHeight()); + g.drawImage(im, 0, 0, null); + g.dispose(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ImageFormGenerator formImageGen = new ImageFormGenerator("form", "title", dimension, im, false); + PSGenerator gen = new PSGenerator(out); + formImageGen.generate(gen); + return out.toString("utf-8"); + } +} diff --git a/src/test/java/org/apache/xmlgraphics/ps/ImageEncodingHelperTestCase.java b/src/test/java/org/apache/xmlgraphics/ps/ImageEncodingHelperTestCase.java new file mode 100644 index 0000000..2d0e978 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/ps/ImageEncodingHelperTestCase.java @@ -0,0 +1,153 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ImageEncodingHelperTestCase.java 1896578 2021-12-31 12:22:29Z ssteiner $ */ + +package org.apache.xmlgraphics.ps; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; +import java.awt.image.DataBuffer; +import java.awt.image.DirectColorModel; +import java.awt.image.WritableRaster; +import java.io.IOException; +import java.util.Arrays; + +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.apache.commons.io.output.ByteArrayOutputStream; + +public class ImageEncodingHelperTestCase { + + private BufferedImage prepareImage(BufferedImage image) { + Graphics2D ig = image.createGraphics(); + ig.scale(.5, .5); + ig.setPaint(new Color(128, 0, 0)); + ig.fillRect(0, 0, 100, 50); + ig.setPaint(Color.orange); + ig.fillRect(100, 0, 100, 50); + ig.setPaint(Color.yellow); + ig.fillRect(0, 50, 100, 50); + ig.setPaint(Color.red); + ig.fillRect(100, 50, 100, 50); + ig.setPaint(new Color(255, 127, 127)); + ig.fillRect(0, 100, 100, 50); + ig.setPaint(Color.black); + ig.draw(new Rectangle2D.Double(0.5, 0.5, 199, 149)); + ig.dispose(); + return image; + } + + /** + * Tests encodeRenderedImageWithDirectColorModeAsRGB(). Tests the optimised method against the + * non-optimised method(encodeRenderedImageAsRGB) to ensure the BufferedImage produced are the + * same. + * @throws IOException if an I/O error occurs. + */ + @Test + public void testEncodeRenderedImageWithDirectColorModelAsRGB() throws IOException { + BufferedImage image = new BufferedImage(100, 75, BufferedImage.TYPE_INT_ARGB); + image = prepareImage(image); + + ByteArrayOutputStream optimized = new ByteArrayOutputStream(); + ImageEncodingHelper.encodeRenderedImageWithDirectColorModelAsRGB(image, optimized); + + ByteArrayOutputStream nonoptimized = new ByteArrayOutputStream(); + ImageEncodingHelper.encodeRenderedImageAsRGB(image, nonoptimized); + + assertTrue(Arrays.equals(nonoptimized.toByteArray(), optimized.toByteArray())); + + } + + /** + * Tests a BGR versus RBG image. Debugging shows the BGR follows the optimizeWriteTo() (which + * is intended). The bytes are compared with the RBG image, which happens to follow the + * writeRGBTo(). + * + * @throws IOException + */ + @Test + public void testRGBAndBGRImages() throws IOException { + BufferedImage imageBGR = new BufferedImage(100, 75, BufferedImage.TYPE_3BYTE_BGR); + imageBGR = prepareImage(imageBGR); + BufferedImage imageRGB = new BufferedImage(100, 75, BufferedImage.TYPE_INT_BGR); + imageRGB = prepareImage(imageRGB); + + ImageEncodingHelper imageEncodingHelperBGR = new ImageEncodingHelper(imageBGR, false); + ImageEncodingHelper imageEncodingHelperRGB = new ImageEncodingHelper(imageRGB, false); + + ByteArrayOutputStream baosBGR = new ByteArrayOutputStream(); + imageEncodingHelperBGR.encode(baosBGR); + + ByteArrayOutputStream baosRGB = new ByteArrayOutputStream(); + imageEncodingHelperRGB.encode(baosRGB); + + assertTrue(Arrays.equals(baosBGR.toByteArray(), baosRGB.toByteArray())); + } + + /** + * Tests encodeRenderedImageWithDirectColorModeAsRGB(). Uses mocking to test the method + * implementation. + * @throws IOException if an I/O error occurs. + */ + @Test + public void testMockedEncodeRenderedImageWithDirectColorModelAsRGB() throws IOException { + BufferedImage image = mock(BufferedImage.class); + final int[] templateMasks = new int[] {0x00ff0000 /*R*/, 0x0000ff00 /*G*/, + 0x000000ff /*B*/, 0xff000000 /*A*/}; + DirectColorModel dcm = new DirectColorModel(255, templateMasks[0], templateMasks[1], + templateMasks[2], templateMasks[3]); + + WritableRaster raster = mock(WritableRaster.class); + DataBuffer buffer = mock(DataBuffer.class); + + when(image.getColorModel()).thenReturn(dcm); + when(image.getRaster()).thenReturn(raster); + when(raster.getDataBuffer()).thenReturn(buffer); + when(buffer.getDataType()).thenReturn(DataBuffer.TYPE_INT); + when(image.getWidth()).thenReturn(3); + when(image.getHeight()).thenReturn(3); + final int expectedValue = 1 + 2 << 8 + 3 << 16; + Answer ans = new Answer() { + public Object answer(InvocationOnMock invocation) throws Throwable { + Object[] args = invocation.getArguments(); + int[] data = (int[]) args[4]; + Arrays.fill(data, expectedValue); + return null; + } + }; + when(raster.getDataElements(anyInt(), anyInt(), anyInt(), anyInt(), any())) + .thenAnswer(ans); + + ByteArrayOutputStream optimized = new ByteArrayOutputStream(); + ImageEncodingHelper.encodeRenderedImageWithDirectColorModelAsRGB(image, optimized); + + byte[] expectedByteArray = new byte[27]; + Arrays.fill(expectedByteArray, (byte) expectedValue); + assertTrue(Arrays.equals(expectedByteArray, optimized.toByteArray())); + } +} diff --git a/src/test/java/org/apache/xmlgraphics/ps/PSEscapeTestCase.java b/src/test/java/org/apache/xmlgraphics/ps/PSEscapeTestCase.java new file mode 100644 index 0000000..074b3c7 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/ps/PSEscapeTestCase.java @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: PSEscapeTestCase.java 1365650 2012-07-25 15:59:30Z mehdi $ */ + +package org.apache.xmlgraphics.ps; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * Tests literal text string escaping. + */ +public class PSEscapeTestCase { + + @Test + public void testBasics() throws Exception { + StringBuffer sb = new StringBuffer(); + + PSGenerator.escapeChar('a', sb); + PSGenerator.escapeChar('b', sb); + PSGenerator.escapeChar('c', sb); + PSGenerator.escapeChar('!', sb); + assertEquals("abc!", sb.toString()); + + sb.setLength(0); + PSGenerator.escapeChar('0', sb); + PSGenerator.escapeChar('\t', sb); + PSGenerator.escapeChar('(', sb); + PSGenerator.escapeChar('x', sb); + PSGenerator.escapeChar(')', sb); + PSGenerator.escapeChar('\n', sb); + PSGenerator.escapeChar('\u001E', sb); // + PSGenerator.escapeChar('\u00E4', sb); //a umlaut + PSGenerator.escapeChar('\u20AC', sb); //EURO Sign + assertEquals("0\\t\\(x\\)\\n\\036\\344?", sb.toString()); + } + + @Test + public void testStringToDSC() throws Exception { + String escaped; + escaped = PSGenerator.convertStringToDSC("0\t(x)\n\u001E\u00E4\u20AC"); + assertEquals("0\\t\\(x\\)\\n\\036\\344?", escaped); + escaped = PSGenerator.convertStringToDSC("0\t(x)\n\u001E\u00E4 \u20AC"); + assertEquals("(0\\t\\(x\\)\\n\\036\\344 ?)", escaped); + escaped = PSGenerator.convertStringToDSC("0\t(x)\n\u001E\u00E4\u20AC", true); + assertEquals("(0\\t\\(x\\)\\n\\036\\344?)", escaped); + } + +} diff --git a/src/test/java/org/apache/xmlgraphics/ps/PSPageDeviceDictionaryTestCase.java b/src/test/java/org/apache/xmlgraphics/ps/PSPageDeviceDictionaryTestCase.java new file mode 100644 index 0000000..45e7125 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/ps/PSPageDeviceDictionaryTestCase.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.xmlgraphics.ps; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.Assert; +import org.junit.Test; + +public class PSPageDeviceDictionaryTestCase { + @Test + public void testPSPageDeviceDictionary() { + PSPageDeviceDictionary p = new PSPageDeviceDictionary(); + p.setFlushOnRetrieval(true); + Map m = new HashMap(); + m.put("x", "y"); + p.putAll(m); + Assert.assertEquals(p.getContent(), "<<\n x y\n>>"); + Assert.assertEquals(p.getContent(), ""); + } +} diff --git a/src/test/java/org/apache/xmlgraphics/ps/dsc/DSCParserTestCase.java b/src/test/java/org/apache/xmlgraphics/ps/dsc/DSCParserTestCase.java new file mode 100644 index 0000000..0dbdbe9 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/ps/dsc/DSCParserTestCase.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DSCParserTestCase.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.ps.dsc; + +import java.io.ByteArrayInputStream; + +import org.junit.Test; + +public class DSCParserTestCase { + + private final String correctDSC + = "%!PS-Adobe-3.0\n" + + "%%LanguageLevel: 3\n" + + "%%EOF"; + + private final String spuriousContentAfterEOF + = "%!PS-Adobe-3.0\n" + + "%%LanguageLevel: 3\n" + + "%%EOF\n" + + "%%SpuriousContent"; + + @Test + public void eofDetectedWhenCheckEOFEnabled() throws Exception { + parseDSC(correctDSC, true); + } + + @Test + public void eofDetectedWhenCheckEOFDisabled() throws Exception { + parseDSC(correctDSC, false); + } + + @Test(expected = DSCException.class) + public void spuriousContentDetected() throws Exception { + parseDSC(spuriousContentAfterEOF, true); + } + + @Test + public void spuriousContentIgnored() throws Exception { + parseDSC(spuriousContentAfterEOF, false); + } + + private void parseDSC(String dsc, boolean checkEOF) throws Exception { + DSCParser parser = new DSCParser(new ByteArrayInputStream(dsc.getBytes("US-ASCII"))); + parser.setCheckEOF(checkEOF); + while (parser.hasNext()) { + parser.next(); + } + } + +} diff --git a/src/test/java/org/apache/xmlgraphics/ps/dsc/ListenerTestCase.java b/src/test/java/org/apache/xmlgraphics/ps/dsc/ListenerTestCase.java new file mode 100644 index 0000000..c340297 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/ps/dsc/ListenerTestCase.java @@ -0,0 +1,130 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ListenerTestCase.java 1681108 2015-05-22 13:26:12Z ssteiner $ */ + +package org.apache.xmlgraphics.ps.dsc; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import org.apache.commons.io.IOUtils; + +import org.apache.xmlgraphics.ps.DSCConstants; +import org.apache.xmlgraphics.ps.dsc.events.DSCComment; +import org.apache.xmlgraphics.ps.dsc.events.DSCCommentLanguageLevel; +import org.apache.xmlgraphics.ps.dsc.events.DSCEvent; + +/** + * Tests the listener functionality on the DSC parser. + */ +public class ListenerTestCase { + + /** + * Tests {@link DSCParser#setFilter(DSCFilter)}. + * @throws Exception if an error occurs + */ + @Test + public void testFilter() throws Exception { + InputStream in = getClass().getResourceAsStream("test1.txt"); + try { + DSCParser parser = new DSCParser(in); + parser.setFilter(new DSCFilter() { + + public boolean accept(DSCEvent event) { + //Filter out all non-DSC comments + return !event.isComment(); + } + + }); + while (parser.hasNext()) { + DSCEvent event = parser.nextEvent(); + + if (parser.getCurrentEvent().isComment()) { + fail("Filter failed. Comment found."); + } + } + } finally { + IOUtils.closeQuietly(in); + } + } + + /** + * Tests listeners on DSCParser. + * @throws Exception if an error occurs + */ + @Test + public void testListeners() throws Exception { + InputStream in = getClass().getResourceAsStream("test1.txt"); + try { + final Map results = new java.util.HashMap(); + DSCParser parser = new DSCParser(in); + + //Filter the prolog + parser.addListener(new DSCListener() { + public void processEvent(DSCEvent event, DSCParser parser) + throws IOException, DSCException { + if (event.isDSCComment()) { + DSCComment comment = event.asDSCComment(); + if (DSCConstants.BEGIN_PROLOG.equals(comment.getName())) { + //Skip until end of prolog + while (parser.hasNext()) { + DSCEvent e = parser.nextEvent(); + if (e.isDSCComment()) { + if (DSCConstants.END_PROLOG.equals( + e.asDSCComment().getName())) { + parser.next(); + break; + } + } + + } + } + } + } + }); + + //Listener for the language level + parser.addListener(new DSCListener() { + public void processEvent(DSCEvent event, DSCParser parser) + throws IOException, DSCException { + if (event instanceof DSCCommentLanguageLevel) { + DSCCommentLanguageLevel level = (DSCCommentLanguageLevel) event; + results.put("level", level.getLanguageLevel()); + } + } + }); + int count = 0; + while (parser.hasNext()) { + DSCEvent event = parser.nextEvent(); + System.out.println(event); + count++; + } + assertEquals(12, count); + assertEquals(1, results.get("level")); + } finally { + IOUtils.closeQuietly(in); + } + } + +} diff --git a/src/test/java/org/apache/xmlgraphics/ps/dsc/events/DSCCommentBoundingBoxTestCase.java b/src/test/java/org/apache/xmlgraphics/ps/dsc/events/DSCCommentBoundingBoxTestCase.java new file mode 100644 index 0000000..7f1d484 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/ps/dsc/events/DSCCommentBoundingBoxTestCase.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DSCCommentBoundingBoxTestCase.java 1365650 2012-07-25 15:59:30Z mehdi $ */ + +package org.apache.xmlgraphics.ps.dsc.events; + +import java.awt.Rectangle; +import java.awt.geom.Rectangle2D; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +import org.apache.xmlgraphics.ps.dsc.DSCCommentFactory; + +public class DSCCommentBoundingBoxTestCase { + + @Test + public void testBoundingBox() throws Exception { + DSCComment comment = DSCCommentFactory.createDSCCommentFor("BoundingBox"); + DSCCommentBoundingBox bbox = (DSCCommentBoundingBox) comment; + bbox.parseValue("289 412 306 429"); + Rectangle refRect = new Rectangle(289, 412, 306 - 289, 429 - 412); + assertEquals(refRect, bbox.getBoundingBox()); + + comment = DSCCommentFactory.createDSCCommentFor("BoundingBox"); + bbox = (DSCCommentBoundingBox) comment; + bbox.parseValue("289.12 412.2 306.777 429.11"); + Rectangle2D refRect2D = new Rectangle2D.Double( + 289.12, 412.2, 306.777 - 289.12, 429.11 - 412.2); + assertEquals(refRect2D, bbox.getBoundingBox()); + + comment = DSCCommentFactory.createDSCCommentFor("HiResBoundingBox"); + bbox = (DSCCommentHiResBoundingBox) comment; + bbox.parseValue("289.12 412.2 306.777 429.11"); + refRect2D = new Rectangle2D.Double( + 289.12, 412.2, 306.777 - 289.12, 429.11 - 412.2); + assertEquals(refRect2D, bbox.getBoundingBox()); + } + +} diff --git a/src/test/java/org/apache/xmlgraphics/ps/dsc/events/DSCValueParserTestCase.java b/src/test/java/org/apache/xmlgraphics/ps/dsc/events/DSCValueParserTestCase.java new file mode 100644 index 0000000..b7aa889 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/ps/dsc/events/DSCValueParserTestCase.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DSCValueParserTestCase.java 1365650 2012-07-25 15:59:30Z mehdi $ */ + +package org.apache.xmlgraphics.ps.dsc.events; + +import java.util.List; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class DSCValueParserTestCase { + + private String[] toArray(List params) { + return (String[]) params.toArray(new String[params.size()]); + } + + @Test + public void testText() throws Exception { + DSCCommentBeginResource obj = new DSCCommentBeginResource(); + String[] res = toArray(obj.splitParams("procset Test")); + assertEquals(2, res.length); + assertEquals("procset", res[0]); + assertEquals("Test", res[1]); + + res = toArray(obj.splitParams("procset\tTest")); + assertEquals(2, res.length); + assertEquals("procset", res[0]); + assertEquals("Test", res[1]); + } + + @Test + public void testParentheseText() throws Exception { + DSCCommentBeginResource obj = new DSCCommentBeginResource(); + String[] res = toArray(obj.splitParams("procset (Hello World!)")); + assertEquals(2, res.length); + assertEquals("procset", res[0]); + assertEquals("Hello World!", res[1]); + + res = toArray(obj.splitParams("procset\t(Hello\t\\\\wonderful/ World!)")); + assertEquals(2, res.length); + assertEquals("procset", res[0]); + assertEquals("Hello\t\\wonderful/ World!", res[1]); + + res = toArray(obj.splitParams("procset (Hello \\042wonderful\\042 World!) blahblah")); + assertEquals(3, res.length); + assertEquals("procset", res[0]); + assertEquals("Hello \"wonderful\" World!", res[1]); + assertEquals("blahblah", res[2]); + + //Parentheses not balanced + res = toArray(obj.splitParams("procset (Hello (wonderful) World! blahblah")); + assertEquals(2, res.length); + assertEquals("procset", res[0]); + assertEquals("Hello (wonderful) World! blahblah", res[1]); + } + +} diff --git a/src/test/java/org/apache/xmlgraphics/ps/dsc/test1.txt b/src/test/java/org/apache/xmlgraphics/ps/dsc/test1.txt new file mode 100644 index 0000000..10697d7 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/ps/dsc/test1.txt @@ -0,0 +1,15 @@ +%!PS-Adobe-3.0 EPSF-3.0 +%%BoundingBox: 0 0 136 43 +%%LanguageLevel: 1 +%Custom comment +%%EndComments +%%BeginProlog +prologing +%%EndProlog +dummy postscript +dummy postscript +%Custom comment +dummy postscript +dummy postscript +dummy postscript +%%EOF diff --git a/src/test/java/org/apache/xmlgraphics/ps/dsc/tools/DSCToolsTestCase.java b/src/test/java/org/apache/xmlgraphics/ps/dsc/tools/DSCToolsTestCase.java new file mode 100644 index 0000000..55d1c34 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/ps/dsc/tools/DSCToolsTestCase.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DSCToolsTestCase.java 1365650 2012-07-25 15:59:30Z mehdi $ */ + +package org.apache.xmlgraphics.ps.dsc.tools; + +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.apache.xmlgraphics.ps.dsc.events.DSCCommentEndComments; +import org.apache.xmlgraphics.ps.dsc.events.DSCCommentPages; +import org.apache.xmlgraphics.ps.dsc.events.DSCEvent; +import org.apache.xmlgraphics.ps.dsc.events.PostScriptComment; +import org.apache.xmlgraphics.ps.dsc.events.PostScriptLine; + +public class DSCToolsTestCase { + + @Test + public void testEndComment() throws Exception { + DSCEvent event; + + event = new DSCCommentEndComments(); + assertTrue(DSCTools.headerCommentsEndHere(event)); + + event = new PostScriptComment("FOPTest"); + assertFalse(DSCTools.headerCommentsEndHere(event)); + + event = new DSCCommentPages(7); + assertFalse(DSCTools.headerCommentsEndHere(event)); + + event = new PostScriptComment(null); + assertTrue(DSCTools.headerCommentsEndHere(event)); + + event = new PostScriptComment("\t"); + assertTrue(DSCTools.headerCommentsEndHere(event)); + + event = new PostScriptComment(" ***"); + assertTrue(DSCTools.headerCommentsEndHere(event)); + + event = new PostScriptLine("/pgsave save def"); + assertTrue(DSCTools.headerCommentsEndHere(event)); + } + +} diff --git a/src/test/java/org/apache/xmlgraphics/util/ClasspathResourceTestCase.java b/src/test/java/org/apache/xmlgraphics/util/ClasspathResourceTestCase.java new file mode 100644 index 0000000..b61d46c --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/util/ClasspathResourceTestCase.java @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ClasspathResourceTestCase.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.util; + +import java.net.URL; +import java.util.Iterator; +import java.util.List; + +import org.junit.Test; + +import static org.junit.Assert.assertTrue; + +/** + * Test for the Service class. + */ +public class ClasspathResourceTestCase { + + /** + * Tests whether the file /sample.txt with mime-type text/plain exists. + * + * @throws Exception + * in case of an error + */ + @Test + public void testSampleResource() throws Exception { + final List list = ClasspathResource.getInstance() + .listResourcesOfMimeType("text/plain"); + boolean found = false; + final Iterator i = list.iterator(); + while (i.hasNext()) { + final URL u = (URL) i.next(); + if (u.getPath().endsWith("sample.txt")) { + found = true; + } + } + assertTrue(found); + } + + /** + * Tests the mode where Service returns class names. + * + * @throws Exception + * in case of an error + */ + @Test + public void testNonexistingResource() throws Exception { + final List list = ClasspathResource.getInstance() + .listResourcesOfMimeType("nota/mime-type"); + assertTrue(list.isEmpty()); + } +} diff --git a/src/test/java/org/apache/xmlgraphics/util/DateFormatUtilTestCase.java b/src/test/java/org/apache/xmlgraphics/util/DateFormatUtilTestCase.java new file mode 100644 index 0000000..afb7efc --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/util/DateFormatUtilTestCase.java @@ -0,0 +1,119 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DateFormatUtilTestCase.java 1365650 2012-07-25 15:59:30Z mehdi $ */ + +package org.apache.xmlgraphics.util; + +import java.util.Calendar; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import org.apache.xmlgraphics.xmp.XMPSchemaAdapter; + +/** + * Tests date formatting for XMP. + */ +public class DateFormatUtilTestCase { + + /** + * Checks date formatting for XMP. + * @throws Exception if an error occurs + */ + @Test + public void testDateFormattingISO8601() throws Exception { + Date dt = createTestDate(); + + String s = XMPSchemaAdapter.formatISO8601Date(dt, TimeZone.getTimeZone("GMT")); + assertEquals("2008-02-07T15:11:07Z", s); + assertEquals(dt, DateFormatUtil.parseISO8601Date(s)); + + s = XMPSchemaAdapter.formatISO8601Date(dt, TimeZone.getTimeZone("GMT+02:00")); + assertEquals("2008-02-07T17:11:07+02:00", s); + assertEquals(dt, DateFormatUtil.parseISO8601Date(s)); + + s = XMPSchemaAdapter.formatISO8601Date(dt, TimeZone.getTimeZone("GMT+02:30")); + assertEquals("2008-02-07T17:41:07+02:30", s); + assertEquals(dt, DateFormatUtil.parseISO8601Date(s)); + + s = XMPSchemaAdapter.formatISO8601Date(dt, TimeZone.getTimeZone("GMT-08:00")); + assertEquals("2008-02-07T07:11:07-08:00", s); + assertEquals(dt, DateFormatUtil.parseISO8601Date(s)); + + s = XMPSchemaAdapter.formatISO8601Date(dt, TimeZone.getTimeZone("GMT-11:00")); + assertEquals("2008-02-07T04:11:07-11:00", s); + assertEquals(dt, DateFormatUtil.parseISO8601Date(s)); + } + + private Date createTestDate() { + Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"), Locale.ENGLISH); + cal.set(2008, Calendar.FEBRUARY, 07, 15, 11, 07); + cal.set(Calendar.MILLISECOND, 0); + Date dt = cal.getTime(); + return dt; + } + + @Test + public void testDateFormattingPDF() throws Exception { + Date dt = createTestDate(); + + String s = DateFormatUtil.formatPDFDate(dt, TimeZone.getTimeZone("GMT")); + assertEquals("D:20080207151107Z", s); + + s = DateFormatUtil.formatPDFDate(dt, TimeZone.getTimeZone("GMT+02:00")); + assertEquals("D:20080207171107+02'00'", s); + + s = DateFormatUtil.formatPDFDate(dt, TimeZone.getTimeZone("GMT+02:30")); + assertEquals("D:20080207174107+02'30'", s); + + s = DateFormatUtil.formatPDFDate(dt, TimeZone.getTimeZone("GMT-08:00")); + assertEquals("D:20080207071107-08'00'", s); + + s = DateFormatUtil.formatPDFDate(dt, TimeZone.getTimeZone("GMT-11:00")); + assertEquals("D:20080207041107-11'00'", s); + } + + @Test + public void testParseInvalidDateNoColonUTC() { + testInvalidDate("2008-02-07T151107Z"); + } + + @Test + public void testParseInvalidDateNoColonLocal() { + testInvalidDate("2008-02-07T151107+0000"); + } + + @Test + public void testParseInvalidDateColonLast() { + testInvalidDate("2008-02-07T151107Z:"); + } + + private void testInvalidDate(String date) { + try { + DateFormatUtil.parseISO8601Date(date); + fail(); + } catch (IllegalArgumentException e) { + // Expected + } + } +} diff --git a/src/test/java/org/apache/xmlgraphics/util/DoubleFormatUtilTestCase.java b/src/test/java/org/apache/xmlgraphics/util/DoubleFormatUtilTestCase.java new file mode 100644 index 0000000..8b99efb --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/util/DoubleFormatUtilTestCase.java @@ -0,0 +1,610 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DoubleFormatUtilTestCase.java 1780540 2017-01-27 11:10:50Z ssteiner $ */ + +package org.apache.xmlgraphics.util; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.util.Locale; +import java.util.Random; + +import org.junit.Ignore; +import org.junit.Test; + +import static org.junit.Assert.assertTrue; + +/** + * Test class of DoubleFormatUtil + */ +public class DoubleFormatUtilTestCase { + + /** + * Test simple values as specified in the format contract. + *

    + * Note: Some of these tests will fail if formatFast is used. + */ + @Test + public void testSimple() { + int decimals = 4; + int precision = 8; + + double value = 0.0; + String expected = "0"; + String actual = format(value, decimals, precision); + assertEquals(value, decimals, precision, expected, actual); + + value = 0.1; + expected = "0.1"; + actual = format(value, decimals, precision); + assertEquals(value, decimals, precision, expected, actual); + + value = 1234.1; + expected = "1234.1"; + actual = format(value, decimals, precision); + assertEquals(value, decimals, precision, expected, actual); + + // rounding + value = 1234.1234567; + expected = "1234.1235"; + actual = format(value, decimals, precision); + assertEquals(value, decimals, precision, expected, actual); + + value = 1234.99995; + expected = "1235"; + actual = format(value, decimals, precision); + assertEquals(value, decimals, precision, expected, actual); + + value = -1234.99995; + expected = "-1235"; + actual = format(value, decimals, precision); + assertEquals(value, decimals, precision, expected, actual); + + value = 1234.99994999; + expected = "1234.9999"; + actual = format(value, decimals, precision); + assertEquals(value, decimals, precision, expected, actual); + + // decimals / precision switch + value = 0.00000001; + expected = "0.00000001"; + actual = format(value, decimals, precision); + assertEquals(value, decimals, precision, expected, actual); + + value = -0.00000001; + expected = "-0.00000001"; + actual = format(value, decimals, precision); + assertEquals(value, decimals, precision, expected, actual); + + value = 72.00001234; + expected = "72"; + actual = format(value, decimals, precision); + assertEquals(value, decimals, precision, expected, actual); + + // limit precision + value = 0.000000001; + expected = "0"; + actual = format(value, decimals, precision); + assertEquals(value, decimals, precision, expected, actual); + + value = 5.0e-9; + expected = "0.00000001"; + actual = format(value, decimals, precision); + assertEquals(value, decimals, precision, expected, actual); + + value = 4.9999999999e-9; + expected = "0"; + actual = format(value, decimals, precision); + assertEquals(value, decimals, precision, expected, actual); + + value = 2.0005e-5; + expected = "0.00002001"; + actual = format(value, decimals, precision); + assertEquals(value, decimals, precision, expected, actual); + + value = 2.00049999999999e-5; + expected = "0.00002"; + actual = format(value, decimals, precision); + assertEquals(value, decimals, precision, expected, actual); + + // Test added after bug #43940 was reopened + value = 0.005859375; + expected = "0.00585938"; + actual = format(value, 8, 8); + assertEquals(value, 8, 8, expected, actual); + + value = 5.22534294505995E-4; + expected = "0.000522534294506"; + actual = format(value, 17, 17); + assertEquals(value, 17, 17, expected, actual); + + value = 4.9E-324; + expected = "0"; + actual = format(value, 309, 309); + assertEquals(value, 309, 309, expected, actual); + + value = 7.003868765287485E-280; + expected = refFormat(value, 294, 294); + actual = format(value, 294, 294); + assertEquals(value, 294, 294, expected, actual); + + value = 5E-304; + expected = refFormat(value, 303, 303); + actual = format(value, 303, 303); + assertEquals(value, 303, 303, expected, actual); + + value = 9.999999999999999E-250; + expected = refFormat(value, 265, 265); + actual = format(value, 265, 265); + assertEquals(value, 265, 265, expected, actual); + } + + @Test + public void testLimits() { + int decimals = 19; + int precision = 19; + + double value = Double.NaN; + String expected = "NaN"; + String actual = format(value, decimals, precision); + assertEquals(value, decimals, precision, expected, actual); + + value = Double.POSITIVE_INFINITY; + expected = "Infinity"; + actual = format(value, decimals, precision); + assertEquals(value, decimals, precision, expected, actual); + + value = Double.NEGATIVE_INFINITY; + expected = "-Infinity"; + actual = format(value, decimals, precision); + assertEquals(value, decimals, precision, expected, actual); + + value = 1e-3 + Double.MIN_VALUE; + expected = "0.001"; + actual = format(value, decimals, precision); + assertEquals(value, decimals, precision, expected, actual); + + value = 1e-3 - Double.MIN_VALUE; + expected = "0.001"; + actual = format(value, decimals, precision); + assertEquals(value, decimals, precision, expected, actual); + + value = 1e-3; + expected = "0.001"; + actual = format(value, decimals, precision); + assertEquals(value, decimals, precision, expected, actual); + + value = 0.0010000000000000002; // == Math.nextAfter(1e-3, Double.POSITIVE_INFINITY); + expected = "0.0010000000000000002"; + actual = format(value, decimals, precision); + assertEquals(value, decimals, precision, expected, actual); + expected = "0.001"; + actual = format(value, 18, 18); + assertEquals(value, 18, 18, expected, actual); + + value = 0.0009999999999999998; // == Math.nextAfter(1e-3, Double.NEGATIVE_INFINITY); + expected = "0.0009999999999999998"; + actual = format(value, decimals, precision); + assertEquals(value, decimals, precision, expected, actual); + expected = "0.001"; + actual = format(value, 18, 18); + assertEquals(value, 18, 18, expected, actual); + + value = 1e7 + Double.MIN_VALUE; + expected = "10000000"; + actual = format(value, decimals, precision); + assertEquals(value, decimals, precision, expected, actual); + + value = 1e7 - Double.MIN_VALUE; + expected = "10000000"; + actual = format(value, decimals, precision); + assertEquals(value, decimals, precision, expected, actual); + + value = 1e7; + expected = "10000000"; + actual = format(value, decimals, precision); + assertEquals(value, decimals, precision, expected, actual); + + value = 1.0000000000000002E7; // == Math.nextAfter(1e7, Double.POSITIVE_INFINITY); + expected = "10000000.000000002"; + actual = format(value, decimals, precision); + assertEquals(value, decimals, precision, expected, actual); + expected = "10000000"; + actual = format(value, 8, 8); + assertEquals(value, 8, 8, expected, actual); + + value = 9999999.999999998; // == Math.nextAfter(1e7, Double.NEGATIVE_INFINITY); + expected = "9999999.999999998"; + actual = format(value, decimals, precision); + assertEquals(value, decimals, precision, expected, actual); + expected = "10000000"; + actual = format(value, 8, 8); + assertEquals(value, 8, 8, expected, actual); + + value = 0.000009999999999999997; // Check higher precision + expected = "0.000009999999999999997"; + actual = format(value, 21, 21); + assertEquals(value, 21, 21, expected, actual); + expected = "0.00001"; + actual = format(value, 20, 20); + assertEquals(value, 20, 20, expected, actual); + } + + /** + * AssertEquals with a more detailed message + */ + private static void assertEquals(double value, int decimals, int precision, String expected, String actual) { + assertTrue("value: " + value + ", decimals: " + decimals + ", precision: " + precision, + expected.equals(actual)); + } + + /** + * The buffer used to format + */ + private StringBuffer buf = new StringBuffer(); + + /** + * Formats using FormatUtil#formatDouble method + */ + private String format(double value, int decimals, int precision) { + buf.setLength(0); + DoubleFormatUtil.formatDouble(value, decimals, precision, buf); + return buf.toString(); + } + + /** + * Formats using FormatUtil#formatDoublePrecise method + */ + private String formatPrecise(double value, int decimals, int precision) { + buf.setLength(0); + DoubleFormatUtil.formatDoublePrecise(value, decimals, precision, buf); + return buf.toString(); + } + + /** + * Formats using FormatUtil#formatDoubleFast method + */ + private String formatFast(double value, int decimals, int precision) { + buf.setLength(0); + DoubleFormatUtil.formatDoubleFast(value, decimals, precision, buf); + return buf.toString(); + } + + /** + * Formats using a BigDecimal. This is the reference (always returns the correct format) + * whereas DecimalFormat may have some formating errors regarding the last digit. + */ + private String refFormat(double value, int decimals, int precision) { + if (Double.isNaN(value) || Double.isInfinite(value)) { + return Double.toString(value); + } + buf.setLength(0); + BigDecimal bg = new BigDecimal(Double.toString(value)); + int scale = Math.abs(value) < 1.0 ? precision : decimals; + bg = bg.setScale(scale, RoundingMode.HALF_UP); + //buf.append(bg.toString()); // Java 1.4 + buf.append(bg.toPlainString()); // Java 1.5 and more ! + if (buf.indexOf(".") >= 0) { + for (int i = buf.length() - 1; i > 1 && buf.charAt(i) == '0'; i--) { + buf.setLength(i); + } + if (buf.charAt(buf.length() - 1) == '.') { + buf.setLength(buf.length() - 1); + } + } + return buf.toString(); + } + + /** + * The decimal format used within formatDf method + */ + private DecimalFormat df = new DecimalFormat("0", new DecimalFormatSymbols(Locale.US)); + + /** + * Formats using DecimalFormat#format method + */ + private String formatDf(double value, int decimals, int precision) { + int scale = Math.abs(value) < 1.0 ? precision : decimals; + df.setMaximumFractionDigits(scale); + return df.format(value); + } + + /** + * The maximum power of ten to use when testing high values double + */ + private static final int MAX_POW = 12; + + /** + * Tests the formatPrecise method against the reference, with random values + */ + @Test + public void testPrecise() { + long seed = System.currentTimeMillis(); + Random r = new Random(); + r.setSeed(seed); + + double value; + double highValue; + double lowValue; + int nbTest = 10000; + int maxDecimals = 12; + + String actual; + String expected; + for (int i = nbTest; i > 0; i--) { + int decimals = r.nextInt(maxDecimals); + int precision = decimals + 3; + value = 1 + r.nextDouble(); // Use decimals and not precision + expected = refFormat(value, decimals, precision); + actual = formatPrecise(value, decimals, precision); + assertEquals(value, decimals, precision, expected, actual); + + highValue = value * DoubleFormatUtil.tenPow(r.nextInt(MAX_POW)); + expected = refFormat(highValue, decimals, precision); + actual = formatPrecise(highValue, decimals, precision); + assertEquals(highValue, decimals, precision, expected, actual); + + lowValue = (value - 1) / 1000; + expected = refFormat(lowValue, decimals, precision); + actual = formatPrecise(lowValue, decimals, precision); + assertEquals(lowValue, decimals, precision, expected, actual); + } + } + + /** + * Tests the format method against the reference, with random values + */ + @Test + public void testFormat() { + long seed = System.currentTimeMillis(); + Random r = new Random(); + r.setSeed(seed); + + double value; + double highValue; + double lowValue; + int nbTest = 10000; + int maxDecimals = 12; + + String actual; + String expected; + for (int i = nbTest; i > 0; i--) { + int decimals = r.nextInt(maxDecimals); + int precision = decimals + 3; + value = 1 + r.nextDouble(); // Use decimals and not precision + expected = refFormat(value, decimals, precision); + actual = format(value, decimals, precision); + assertEquals(value, decimals, precision, expected, actual); + + highValue = value * DoubleFormatUtil.tenPow(r.nextInt(MAX_POW)); + expected = refFormat(highValue, decimals, precision); + actual = format(highValue, decimals, precision); + assertEquals(highValue, decimals, precision, expected, actual); + + lowValue = (value - 1) / 1000; + expected = refFormat(lowValue, decimals, precision); + actual = format(lowValue, decimals, precision); + assertEquals(lowValue, decimals, precision, expected, actual); + } + } + + /** + * Tests the formatFast method against the reference, with random values. + * Disabled since the formatFast method is not accurate. + */ + @Test + @Ignore("Disabled since the formatFast method is not accurate.") + public void fast() { + long seed = System.currentTimeMillis(); + Random r = new Random(); + r.setSeed(seed); + + double value; + double highValue; + double lowValue; + int nbTest = 10000; + int maxDecimals = 12; + + String actual; + String expected; + for (int i = nbTest; i > 0; i--) { + int decimals = r.nextInt(maxDecimals); + int precision = decimals + 3; + value = 1 + r.nextDouble(); // Use decimals and not precision + expected = refFormat(value, decimals, precision); + actual = formatFast(value, decimals, precision); + assertEquals(value, decimals, precision, expected, actual); + + highValue = value * DoubleFormatUtil.tenPow(r.nextInt(MAX_POW)); + expected = refFormat(highValue, decimals, precision); + actual = formatFast(highValue, decimals, precision); + System.out.println(expected); + System.out.println(actual); + assertEquals(highValue, decimals, precision, expected, actual); + + lowValue = (value - 1) / 1000; + expected = refFormat(lowValue, decimals, precision); + actual = formatFast(lowValue, decimals, precision); + assertEquals(lowValue, decimals, precision, expected, actual); + } + } + + /** + * Performance comparison of the differents formatXXX methods, + * to see which one is the fastest in the same conditions. + */ + @Test + @Ignore("Disabled since this doesn't test correctness.") + public void performanceCompare() { + // Rename this method in testPerformanceCompare to run it within JUnit tests + // This method is quite long (depends of the value of nbTest). + long seed = System.currentTimeMillis(); + Random r = new Random(); + r.setSeed(seed); + + double value; + double highValue; + double lowValue; + long start = System.currentTimeMillis(); + int nbTest = 1000000; + int maxDecimals = 16; + + r.setSeed(seed); + start = System.currentTimeMillis(); + for (int i = nbTest; i > 0; i--) { + int decimals = r.nextInt(maxDecimals); + int precision = decimals + 3; + value = 1 + r.nextDouble(); // Use decimals and not precision + format(value, decimals, precision); + + highValue = value * DoubleFormatUtil.tenPow(r.nextInt(MAX_POW)); + format(highValue, decimals, precision); + + lowValue = (value - 1) / 1000; + format(lowValue, decimals, precision); + } + long formatDuration = System.currentTimeMillis() - start; + System.out.println("Format duration: " + formatDuration + "ms to format " + (3 * nbTest) + " doubles"); + + r.setSeed(seed); + start = System.currentTimeMillis(); + for (int i = nbTest; i > 0; i--) { + int decimals = r.nextInt(maxDecimals); + int precision = decimals + 3; + value = 1 + r.nextDouble(); // Use decimals and not precision + formatPrecise(value, decimals, precision); + + highValue = value * DoubleFormatUtil.tenPow(r.nextInt(MAX_POW)); + formatPrecise(highValue, decimals, precision); + + lowValue = (value - 1) / 1000; + formatPrecise(lowValue, decimals, precision); + } + long preciseFormatDuration = System.currentTimeMillis() - start; + System.out.println("Format Precise duration: " + preciseFormatDuration + + "ms to format " + (3 * nbTest) + " doubles"); + + r.setSeed(seed); + start = System.currentTimeMillis(); + for (int i = nbTest; i > 0; i--) { + int decimals = r.nextInt(maxDecimals); + int precision = decimals + 3; + value = 1 + r.nextDouble(); // Use decimals and not precision + formatFast(value, decimals, precision); + + highValue = value * DoubleFormatUtil.tenPow(r.nextInt(MAX_POW)); + formatFast(highValue, decimals, precision); + + lowValue = (value - 1) / 1000; + formatFast(lowValue, decimals, precision); + } + long fastFormatDuration = System.currentTimeMillis() - start; + System.out.println("Fast Format duration: " + fastFormatDuration + "ms to format " + (3 * nbTest) + " doubles"); + + r.setSeed(seed); + start = System.currentTimeMillis(); + for (int i = nbTest; i > 0; i--) { + int decimals = r.nextInt(maxDecimals); + int precision = decimals + 3; + value = 1 + r.nextDouble(); // Use decimals and not precision + refFormat(value, decimals, precision); + + highValue = value * DoubleFormatUtil.tenPow(r.nextInt(MAX_POW)); + refFormat(highValue, decimals, precision); + + lowValue = (value - 1) / 1000; + refFormat(lowValue, decimals, precision); + } + long bgDuration = System.currentTimeMillis() - start; + System.out.println("BigDecimal format duration: " + bgDuration + "ms to format " + (3 * nbTest) + " doubles"); + + r.setSeed(seed); + start = System.currentTimeMillis(); + for (int i = nbTest; i > 0; i--) { + int decimals = r.nextInt(maxDecimals); + int precision = decimals + 3; + value = 1 + r.nextDouble(); // Use decimals and not precision + formatDf(value, decimals, precision); + + highValue = value * DoubleFormatUtil.tenPow(r.nextInt(MAX_POW)); + formatDf(highValue, decimals, precision); + + lowValue = (value - 1) / 1000; + formatDf(lowValue, decimals, precision); + } + long dfDuration = System.currentTimeMillis() - start; + System.out.println("DecimalFormat duration: " + dfDuration + "ms to format " + (3 * nbTest) + " doubles"); + + r.setSeed(seed); + start = System.currentTimeMillis(); + for (int i = nbTest; i > 0; i--) { + int decimals = r.nextInt(maxDecimals); + int precision = decimals + 3; + precision++; // Avoid warning unused local variable + value = 1 + r.nextDouble(); // Use decimals and not precision + Double.toString(value); + + highValue = value * DoubleFormatUtil.tenPow(r.nextInt(MAX_POW)); + Double.toString(highValue); + + lowValue = (value - 1) / 1000; + Double.toString(lowValue); + } + long toStringDuration = System.currentTimeMillis() - start; + System.out.println("toString duration: " + toStringDuration + "ms to format " + (3 * nbTest) + " doubles"); + } + + @Test + public void testAllDoubleRanges() { + double[] values = {0, 1, 5, 4.9999, 5.0001, 9.9999, 1234567890, 0 /* The last one is random */}; + Random r = new Random(); + double value; + String expected; + String actual; + int minScale; + int maxScale; + for (int i = -330; i <= 315; i++) { + values[values.length - 1] = r.nextDouble(); + double pow = Math.pow(10.0, i); + for (double d : values) { + value = d * pow; + minScale = 1; + maxScale = 350; + // Reduce scales (unnecessary tests) + if (i < -30) { + minScale = -i - 30; + maxScale = -i + 30; + } else if (i <= 0) { + minScale = 1; + maxScale = -i + 30; + } else { + minScale = 1; + maxScale = 30; + } + for (int scale = minScale; scale <= maxScale; scale++) { + expected = refFormat(value, scale, scale); + actual = format(value, scale, scale); + assertEquals(value, scale, scale, expected, actual); + } + } + + } + } +} diff --git a/src/test/java/org/apache/xmlgraphics/util/HexUtil.java b/src/test/java/org/apache/xmlgraphics/util/HexUtil.java new file mode 100644 index 0000000..f9021a3 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/util/HexUtil.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: HexUtil.java 1365650 2012-07-25 15:59:30Z mehdi $ */ + +package org.apache.xmlgraphics.util; + +/** + * Provides helper functions for converting hexadecimal strings. + */ +public final class HexUtil { + + private HexUtil() { + } + + private static final char[] DIGITS = + {'0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; + + /** + * Converts a byte array to a hexadecimal String + * @param data the data to encode + * @return String the resulting String + */ + public static String toHex(byte[] data) { + final StringBuffer sb = new StringBuffer(data.length * 2); + for (int i = 0; i < data.length; i++) { + sb.append(DIGITS[(data[i] >>> 4) & 0x0F]); + sb.append(DIGITS[data[i] & 0x0F]); + } + return sb.toString(); + } + +} diff --git a/src/test/java/org/apache/xmlgraphics/util/ServiceTestCase.java b/src/test/java/org/apache/xmlgraphics/util/ServiceTestCase.java new file mode 100644 index 0000000..2d3f3f2 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/util/ServiceTestCase.java @@ -0,0 +1,112 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ServiceTestCase.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.util; + +import java.util.Iterator; + +import org.junit.Test; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import org.apache.xmlgraphics.image.writer.ImageWriter; + +/** + * Test for the Service class. + */ +public class ServiceTestCase { + + /** + * Tests the mode where Service returns instances. + * @throws Exception in case of an error + */ + @Test + public void testWithInstances() throws Exception { + Class cls = ImageWriter.class; + boolean found = false; + Object writer1 = null; + Object writer2 = null; + + //First run: Find a writer implementation (one of the two must be available) + Iterator iter = Service.providers(cls); + while (iter.hasNext()) { + Object obj = iter.next(); + assertNotNull(obj); + String className = obj.getClass().getName(); + if ("org.apache.xmlgraphics.image.writer.internal.PNGImageWriter".equals(className)) { + writer1 = obj; + found = true; + break; + } else if ("org.apache.xmlgraphics.image.writer.imageio.ImageIOPNGImageWriter".equals( + className)) { + writer2 = obj; + found = true; + break; + } + } + assertTrue("None of the expected classes found", found); + + //Second run: verify that the same instances are returned + iter = Service.providers(cls); + while (iter.hasNext()) { + Object obj = iter.next(); + assertNotNull(obj); + String className = obj.getClass().getName(); + if ("org.apache.xmlgraphics.image.writer.internal.PNGImageWriter".equals(className)) { + assertTrue(obj == writer1); + break; + } else if ("org.apache.xmlgraphics.image.writer.imageio.ImageIOPNGImageWriter".equals( + className)) { + assertTrue(obj == writer2); + break; + } + } + } + + /** + * Tests the mode where Service returns class names. + * @throws Exception in case of an error + */ + @Test + public void testWithClassNames() throws Exception { + Class cls = ImageWriter.class; + boolean found = true; + Iterator iter = Service.providerNames(cls); + while (iter.hasNext()) { + Object obj = iter.next(); + assertNotNull(obj); + assertTrue("Returned object must be a class name", obj instanceof String); + if ("org.apache.xmlgraphics.image.writer.internal.PNGImageWriter".equals(obj) + || ("org.apache.xmlgraphics.image.writer.imageio.ImageIOPNGImageWriter".equals( + obj))) { + found = true; + } + } + assertTrue("None of the expected classes found", found); + + //Do it a second time to make sure the cache works as expected + iter = Service.providerNames(cls); + while (iter.hasNext()) { + Object obj = iter.next(); + assertNotNull(obj); + assertTrue("Returned object must be a class name", obj instanceof String); + } + } +} diff --git a/src/test/java/org/apache/xmlgraphics/util/UnitConvTestCase.java b/src/test/java/org/apache/xmlgraphics/util/UnitConvTestCase.java new file mode 100644 index 0000000..11c902c --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/util/UnitConvTestCase.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: UnitConvTestCase.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.util; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * Test class for UnitConv. + */ +public class UnitConvTestCase { + + /** + * Test all kinds of unit conversions. + * @throws Exception if the test fails + */ + @Test + public void testConversions() throws Exception { + assertEquals("in2mm", 25.4, UnitConv.in2mm(1), 0.00001); + assertEquals("mm2in", 1.0, UnitConv.mm2in(25.4), 0.00001); + assertEquals("mm2pt", 841.890, UnitConv.mm2pt(297), 0.001 / 2); //height of an A4 page + assertEquals("mm2mpt", 841890, UnitConv.mm2mpt(297), 1.0 / 2); + assertEquals("pt2mm", 297, UnitConv.pt2mm(841.890), 0.0001); + assertEquals("in2mpt", 792000, UnitConv.in2mpt(11.0), 1.0 / 2); //height of a letter page + assertEquals("mpt2in", 11.0, UnitConv.mpt2in(792000), 0.01 / 2); //height of a letter page + + assertEquals("mm2px/72dpi", 841.8897764234434, UnitConv.mm2px(297.0, 72), 0); + assertEquals("mm2px/300dpi", 3507.8740684310146, UnitConv.mm2px(297.0, 300), 0); + } +} diff --git a/src/test/java/org/apache/xmlgraphics/util/io/ASCII85InputStreamTestCase.java b/src/test/java/org/apache/xmlgraphics/util/io/ASCII85InputStreamTestCase.java new file mode 100644 index 0000000..f9a10c4 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/util/io/ASCII85InputStreamTestCase.java @@ -0,0 +1,164 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ASCII85InputStreamTestCase.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.util.io; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.io.output.ByteArrayOutputStream; + +import org.apache.xmlgraphics.util.HexUtil; + +/** + * Test case for ASCII85InputStream. + *

    + * ATTENTION: Some of the tests here depend on the correct behaviour of + * ASCII85OutputStream. If something fails here make sure + * ASCII85OutputStreamTestCase runs! + */ +public class ASCII85InputStreamTestCase { + + private static final boolean DEBUG = false; + + private byte[] decode(String text) throws Exception { + byte[] ascii85 = text.getBytes("US-ASCII"); + InputStream in = new ByteArrayInputStream(ascii85); + InputStream decoder = new ASCII85InputStream(in); + return IOUtils.toByteArray(decoder); + } + + private byte[] getChunk(int count) { + byte[] buf = new byte[count]; + System.arraycopy(ASCII85OutputStreamTestCase.DATA, 0, buf, 0, buf.length); + return buf; + } + + private String encode(byte[] data, int len) throws Exception { + ByteArrayOutputStream baout = new ByteArrayOutputStream(); + java.io.OutputStream out = new ASCII85OutputStream(baout); + out.write(data, 0, len); + out.close(); + return new String(baout.toByteArray(), "US-ASCII"); + } + + + private void innerTestDecode(byte[] data) throws Exception { + String encoded = encode(data, data.length); + if (DEBUG) { + if (data[0] == 0) { + System.out.println("self-encode: " + data.length + " chunk 000102030405..."); + } else { + System.out.println("self-encode: " + new String(data, "US-ASCII") + + " " + HexUtil.toHex(data)); + } + System.out.println(" ---> " + encoded); + } + byte[] decoded = decode(encoded); + if (DEBUG) { + if (data[0] == 0) { + System.out.println("decoded: " + data.length + " chunk 000102030405..."); + } else { + System.out.println("decoded: " + new String(decoded, "US-ASCII") + + " " + HexUtil.toHex(decoded)); + } + } + assertEquals(HexUtil.toHex(data), HexUtil.toHex(decoded)); + } + + /** + * Tests the output of ASCII85. + * @throws Exception if an error occurs + */ + @Test + public void testDecode() throws Exception { + innerTestDecode("1. Bodypart".getBytes("US-ASCII")); + if (DEBUG) { + System.out.println("==========================================="); + } + + innerTestDecode(getChunk(1)); + innerTestDecode(getChunk(2)); + innerTestDecode(getChunk(3)); + innerTestDecode(getChunk(4)); + innerTestDecode(getChunk(5)); + if (DEBUG) { + System.out.println("==========================================="); + } + + innerTestDecode(getChunk(10)); + innerTestDecode(getChunk(62)); + innerTestDecode(getChunk(63)); + innerTestDecode(getChunk(64)); + innerTestDecode(getChunk(65)); + + if (DEBUG) { + System.out.println("==========================================="); + } + String sz; + sz = HexUtil.toHex(decode("zz~>")); + assertEquals(HexUtil.toHex(new byte[] {0, 0, 0, 0, 0, 0, 0, 0}), sz); + sz = HexUtil.toHex(decode("z\t \0z\n~>")); + assertEquals(HexUtil.toHex(new byte[] {0, 0, 0, 0, 0, 0, 0, 0}), sz); + if (DEBUG) { + System.out.println("==========================================="); + } + try { + decode("vz~>"); + fail("Illegal character should be detected"); + } catch (IOException ioe) { + //expected + } + /* DISABLED because of try/catch in InputStream.read(byte[], int, int). + * Only the exception happening on the first byte in a block is being + * reported. BUG in JDK??? + * + try { + decode("zv~>"); + fail("Illegal character should be detected"); + } catch (IOException ioe) { + //expected + }*/ + } + + private byte[] getFullASCIIRange() { + java.io.ByteArrayOutputStream baout = new java.io.ByteArrayOutputStream(256); + for (int i = 254; i < 256; i++) { + baout.write(i); + } + return baout.toByteArray(); + } + + /** + * Tests the full 8-bit ASCII range. + * @throws Exception if an error occurs + */ + @Test + public void testFullASCIIRange() throws Exception { + innerTestDecode(getFullASCIIRange()); + } + +} diff --git a/src/test/java/org/apache/xmlgraphics/util/io/ASCII85OutputStreamTestCase.java b/src/test/java/org/apache/xmlgraphics/util/io/ASCII85OutputStreamTestCase.java new file mode 100644 index 0000000..65ce5fb --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/util/io/ASCII85OutputStreamTestCase.java @@ -0,0 +1,96 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: ASCII85OutputStreamTestCase.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.util.io; + +import java.io.OutputStream; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +import org.apache.commons.io.output.ByteArrayOutputStream; + +/** + * Test case for ASCII85OutputStream + */ +public class ASCII85OutputStreamTestCase { + + /** Test data */ + public static final byte[] DATA = new byte[100]; + + static { + //Fill in some data + for (int i = 0; i < 100; i++) { + DATA[i] = (byte) i; + } + } + + private String encode(int count) throws Exception { + return encode(DATA, count); + } + + private String encode(byte[] data, int len) throws Exception { + ByteArrayOutputStream baout = new ByteArrayOutputStream(); + OutputStream out = new ASCII85OutputStream(baout); + out.write(data, 0, len); + out.close(); + return new String(baout.toByteArray(), "US-ASCII"); + } + + /** + * Tests the output of ASCII85. + * @throws Exception if an error occurs + */ + @Test + public void testOutput() throws Exception { + String sz = encode(new byte[] {0, 0, 0, 0, 0, 0, 0, 0}, 8); + assertEquals("zz~>", sz); + + String s3 = encode(3); + //System.out.println(">>>" + s3 + "<<<"); + assertEquals("!!*-~>", s3); + + String s10 = encode(10); + //System.out.println(">>>" + s10 + "<<<"); + assertEquals("!!*-'\"9eu7#RL~>", s10); + + String s62 = encode(62); + //System.out.println(">>>" + s62 + "<<<"); + assertEquals("!!*-'\"9eu7#RLhG$k3[W&.oNg'GVB\"(`=52*$$(B+<_pR," + + "UFcb-n-Vr/1iJ-0JP==1c70M3&s#]4?W~>", s62); + + String s63 = encode(63); + //System.out.println(">>>" + s63 + "<<<"); + assertEquals("!!*-'\"9eu7#RLhG$k3[W&.oNg'GVB\"(`=52*$$(B+<_pR," + + "UFcb-n-Vr/1iJ-0JP==1c70M3&s#]4?Yk\n~>", s63); + + String s64 = encode(64); + //System.out.println(">>>" + s64 + "<<<"); + assertEquals("!!*-'\"9eu7#RLhG$k3[W&.oNg'GVB\"(`=52*$$(B+<_pR," + + "UFcb-n-Vr/1iJ-0JP==1c70M3&s#]4?Ykm\n~>", s64); + + String s65 = encode(65); + //System.out.println(">>>" + s65 + "<<<"); + assertEquals("!!*-'\"9eu7#RLhG$k3[W&.oNg'GVB\"(`=52*$$(B+<_pR," + + "UFcb-n-Vr/1iJ-0JP==1c70M3&s#]4?Ykm\n5Q~>", s65); + + } + +} diff --git a/src/test/java/org/apache/xmlgraphics/util/io/Base64TestCase.java b/src/test/java/org/apache/xmlgraphics/util/io/Base64TestCase.java new file mode 100644 index 0000000..b66c397 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/util/io/Base64TestCase.java @@ -0,0 +1,268 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: Base64TestCase.java 1876186 2020-04-06 13:37:10Z ssteiner $ */ + +package org.apache.xmlgraphics.util.io; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.net.URL; + +import org.junit.Test; + +import static org.junit.Assert.fail; + +import org.apache.commons.io.IOUtils; + +/** + * This test validates that the Base64 encoder/decoders work properly. + * + * @version $Id: Base64TestCase.java 1876186 2020-04-06 13:37:10Z ssteiner $ + */ +public class Base64TestCase { + + private void innerBase64Test(String action, URL in, URL ref) throws Exception { + InputStream inIS = dos2Unix(in); + + if (action.equals("ROUND")) { + ref = in; + } else if (!action.equals("ENCODE") && !action.equals("DECODE")) { + fail("Bad action string"); + } + + InputStream refIS = dos2Unix(ref); + + if (action.equals("ENCODE") || action.equals("ROUND")) { + // We need to encode the incomming data + PipedOutputStream pos = new PipedOutputStream(); + OutputStream os = new Base64EncodeStream(pos); + + // Copy the input to the Base64 Encoder (in a seperate thread). + Thread t = new StreamCopier(inIS, os); + + // Read that from the piped output stream. + inIS = new PipedInputStream(pos); + t.start(); + } + + if (action.equals("DECODE") || action.equals("ROUND")) { + inIS = new Base64DecodeStream(inIS); + } + + + int mismatch = compareStreams(inIS, refIS, action.equals("ENCODE")); + + if (mismatch != -1) { + fail("Wrong result"); + } + } + + private InputStream dos2Unix(URL url) throws IOException { + InputStream is = url.openStream(); + byte[] data = IOUtils.toByteArray(is); + if (data.length > 1 && data[data.length - 1] == '\n') { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + for (byte b : data) { + if (b != '\r') { + bos.write(b); + } + } + return new ByteArrayInputStream(bos.toByteArray()); + } + return new ByteArrayInputStream(data); + } + + private void innerBase64Test(String action, String in, String ref) throws Exception { + final String baseURL = "file:src/test/resources/org/apache/xmlgraphics/util/io/"; + innerBase64Test(action, new URL(baseURL + in), new URL(baseURL + ref)); + } + + private void innerBase64Test(String in) throws Exception { + innerBase64Test("ROUND", in, in); + } + + private void testBase64Group(String name) throws Exception { + innerBase64Test("ENCODE", name, name + ".64"); + innerBase64Test("DECODE", name + ".64", name); + innerBase64Test(name); + } + + /** + * This method will only throw exceptions if some aspect + * of the test's internal operation fails. + */ + @Test + public void testBase64() throws Exception { + System.out.println(new File(".").getCanonicalPath()); + testBase64Group("zeroByte"); + testBase64Group("oneByte"); + testBase64Group("twoByte"); + testBase64Group("threeByte"); + testBase64Group("fourByte"); + testBase64Group("tenByte"); + testBase64Group("small"); + testBase64Group("medium"); + innerBase64Test("DECODE", "medium.pc.64", "medium"); + innerBase64Test("large"); +} + + /** + * Returns true if the contents of is1 match the + * contents of is2 + */ + public static int compareStreams(InputStream is1, InputStream is2, + boolean skipws) { + byte[] data1 = new byte[100]; + byte[] data2 = new byte[100]; + int off1 = 0; + int off2 = 0; + int idx = 0; + + try { + while (true) { + int len1 = is1.read(data1, off1, data1.length - off1); + int len2 = is2.read(data2, off2, data2.length - off2); + + if (off1 != 0) { + if (len1 == -1) { + len1 = off1; + } else { + len1 += off1; + } + } + + if (off2 != 0) { + if (len2 == -1) { + len2 = off2; + } else { + len2 += off2; + } + } + + if (len1 == -1) { + if (len2 == -1) { + break; // Both done... + } + // Only is1 is done... + if (!skipws) { + return idx; + } + // check if the rest of is2 is whitespace... + for (int i2 = 0; i2 < len2; i2++) { + if ((data2[i2] != '\n') && (data2[i2] != '\r') && (data2[i2] != ' ')) { + return idx + i2; + } + } + off1 = off2 = 0; + continue; + } + + if (len2 == -1) { + // Only is2 is done... + if (!skipws) { + return idx; + } + + // Check if rest of is1 is whitespace... + for (int i1 = 0; i1 < len1; i1++) { + if ((data1[i1] != '\n') && (data1[i1] != '\r') && (data1[i1] != ' ')) { + return idx + i1; + } + } + off1 = off2 = 0; + continue; + } + + int i1 = 0; + int i2 = 0; + while ((i1 < len1) && (i2 < len2)) { + if (skipws) { + if ((data1[i1] == '\n') || (data1[i1] == '\r') || (data1[i1] == ' ')) { + i1++; + continue; + } + if ((data2[i2] == '\n') || (data2[i2] == '\r') || (data2[i2] == ' ')) { + i2++; + continue; + } + } + if (data1[i1] != data2[i2]) { + return idx + i2; + } + + i1++; + i2++; + } + + if (i1 != len1) { + System.arraycopy(data1, i1, data1, 0, len1 - i1); + } + if (i2 != len2) { + System.arraycopy(data2, i2, data2, 0, len2 - i2); + } + off1 = len1 - i1; + off2 = len2 - i2; + idx += i2; + } + } catch (IOException ioe) { + ioe.printStackTrace(); + return idx; + } + + return -1; + } + + + static class StreamCopier extends Thread { + InputStream src; + OutputStream dst; + + public StreamCopier(InputStream src, + OutputStream dst) { + this.src = src; + this.dst = dst; + } + + public void run() { + try { + byte[] data = new byte[1000]; + while (true) { + int len = src.read(data, 0, data.length); + if (len == -1) { + break; + } + + dst.write(data, 0, len); + } + } catch (IOException ioe) { + // Nothing + } + try { + dst.close(); + } catch (IOException ioe) { + // Nothing + } + } + } +} diff --git a/src/test/java/org/apache/xmlgraphics/util/io/SubInputStreamTestCase.java b/src/test/java/org/apache/xmlgraphics/util/io/SubInputStreamTestCase.java new file mode 100644 index 0000000..1b55269 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/util/io/SubInputStreamTestCase.java @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: SubInputStreamTestCase.java 1365650 2012-07-25 15:59:30Z mehdi $ */ + +package org.apache.xmlgraphics.util.io; + +import java.io.ByteArrayInputStream; +import java.util.Arrays; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * Test case for SubInputStream. + */ +public class SubInputStreamTestCase { + + /** + * Tests SubInputStream. + * @throws Exception if an error occurs + */ + @Test + public void testMain() throws Exception { + //Initialize test data + byte[] data = new byte[256]; + for (int i = 0; i < data.length; i++) { + data[i] = (byte) (i & 0xff); + } + + int v; + int c; + byte[] buf; + String s; + + SubInputStream subin = new SubInputStream(new ByteArrayInputStream(data), 10); + v = subin.read(); + assertEquals(0, v); + v = subin.read(); + assertEquals(1, v); + + buf = new byte[4]; + c = subin.read(buf); + assertEquals(4, c); + s = new String(buf, "US-ASCII"); + assertEquals("\u0002\u0003\u0004\u0005", s); + + Arrays.fill(buf, (byte) 0); + c = subin.read(buf, 2, 2); + assertEquals(2, c); + s = new String(buf, "US-ASCII"); + assertEquals("\u0000\u0000\u0006\u0007", s); + + Arrays.fill(buf, (byte) 0); + c = subin.read(buf); + assertEquals(2, c); + s = new String(buf, "US-ASCII"); + assertEquals("\u0008\u0009\u0000\u0000", s); + subin.close(); + } + +} diff --git a/src/test/java/org/apache/xmlgraphics/util/uri/CommonURIResolverTestCase.java b/src/test/java/org/apache/xmlgraphics/util/uri/CommonURIResolverTestCase.java new file mode 100644 index 0000000..ef6cce7 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/util/uri/CommonURIResolverTestCase.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: CommonURIResolverTestCase.java 1732018 2016-02-24 04:51:06Z gadams $ */ + +package org.apache.xmlgraphics.util.uri; + +import javax.xml.transform.URIResolver; + +import org.junit.Test; + +/** + * Test case for the {@link CommonURIResolver}. + */ +public class CommonURIResolverTestCase { + + /** + * Test the DataURIResolver with correct values. + * + * @throws Exception + * if an error occurs + */ + + @Test + public void testDataURLHandling() throws Exception { + URIResolver resolver = CommonURIResolver.getDefaultURIResolver(); + DataURIResolverTestCase.actualURLHAndlingTest(resolver); + } + +} diff --git a/src/test/java/org/apache/xmlgraphics/util/uri/DataURIResolverTestCase.java b/src/test/java/org/apache/xmlgraphics/util/uri/DataURIResolverTestCase.java new file mode 100644 index 0000000..f4fc6b4 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/util/uri/DataURIResolverTestCase.java @@ -0,0 +1,162 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: DataURIResolverTestCase.java 1365650 2012-07-25 15:59:30Z mehdi $ */ + +package org.apache.xmlgraphics.util.uri; + +import java.io.ByteArrayInputStream; + +import javax.xml.transform.Source; +import javax.xml.transform.URIResolver; +import javax.xml.transform.stream.StreamSource; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import org.apache.commons.io.IOUtils; + +/** + * Test case for the RFC 2397 data URL/URI resolver. + */ +public class DataURIResolverTestCase { + + private static final byte[] TESTDATA = new byte[] {0, 1, 2, 3, 4, 5}; + + /** + * Tests DataURLUtil. + * + * @throws Exception + * if an error occurs + */ + @Test + public void testRFC2397Generator() throws Exception { + String url = DataURLUtil.createDataURL(new ByteArrayInputStream( + TESTDATA), null); + assertEquals("Generated data URL is wrong", "data:;base64,AAECAwQF", + url); + + url = DataURLUtil.createDataURL(new ByteArrayInputStream(TESTDATA), + "application/pdf"); + assertEquals("Generated data URL is wrong", + "data:application/pdf;base64,AAECAwQF", url); + } + + /** + * Test the URIResolver contract if the protocol doesn't match. Resolver + * must return null in this case. + * + * @throws Exception + * if an error occurs + */ + @Test + public void testNonMatchingContract() throws Exception { + URIResolver resolver = new DataURIResolver(); + Source src; + + src = resolver.resolve("http://xmlgraphics.apache.org/fop/index.html", + null); + assertNull(src); + + src = resolver.resolve("index.html", + "http://xmlgraphics.apache.org/fop/"); + assertNull(src); + } + + private static boolean byteCmp(byte[] src, int srcOffset, byte[] cmp) { + for (int i = 0, c = cmp.length; i < c; i++) { + if (src[srcOffset + i] != cmp[i]) { + return false; + } + } + return true; + } + + /** + * Test the DataURIResolver with correct values. + * + * @throws Exception + * if an error occurs + */ + @Test + public void testDataURLHandling() throws Exception { + URIResolver resolver = new DataURIResolver(); + actualURLHAndlingTest(resolver); + } + + static final void actualURLHAndlingTest(URIResolver resolver) + throws Exception { + Source src; + + src = resolver.resolve("data:;base64,AAECAwQF", null); + assertNotNull(src); + StreamSource streamSource = (StreamSource) src; + byte[] data = IOUtils.toByteArray(streamSource.getInputStream()); + assertTrue("Decoded data doesn't match the test data", byteCmp( + TESTDATA, 0, data)); + + src = resolver + .resolve( + "data:application/octet-stream;interpreter=fop;base64,AAECAwQF", + null); + assertNotNull(src); + streamSource = (StreamSource) src; + assertNotNull(streamSource.getInputStream()); + assertNull(streamSource.getReader()); + data = IOUtils.toByteArray(streamSource.getInputStream()); + assertTrue("Decoded data doesn't match the test data", byteCmp( + TESTDATA, 0, data)); + + src = resolver.resolve("data:,FOP", null); + assertNotNull(src); + streamSource = (StreamSource) src; + assertNull(streamSource.getInputStream()); + assertNotNull(streamSource.getReader()); + String text = IOUtils.toString(streamSource.getReader()); + assertEquals("FOP", text); + + src = resolver.resolve("data:,A%20brief%20note", null); + assertNotNull(src); + streamSource = (StreamSource) src; + text = IOUtils.toString(streamSource.getReader()); + assertEquals("A brief note", text); + + src = resolver.resolve("data:text/plain;charset=iso-8859-7,%be%f9%be", null); + assertNotNull(src); + streamSource = (StreamSource) src; + text = IOUtils.toString(streamSource.getReader()); + assertEquals("\u038e\u03c9\u038e", text); + } + + /** + * Test that the system Id is not null for the resulting stream objects + * @throws Exception If an error occurs. + */ + @Test + public void testSystemIdForNull() throws Exception { + URIResolver resolver = new DataURIResolver(); + Source source = resolver.resolve("data:;base64,AAECAwQF", null); + assertNotNull(source.getSystemId()); + + source = resolver.resolve("data:text/plain;charset=iso-8859-7,%be%f9%be", null); + assertNotNull(source.getSystemId()); + } +} diff --git a/src/test/java/org/apache/xmlgraphics/xmp/XMPParserTestCase.java b/src/test/java/org/apache/xmlgraphics/xmp/XMPParserTestCase.java new file mode 100644 index 0000000..1fce472 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/xmp/XMPParserTestCase.java @@ -0,0 +1,211 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: XMPParserTestCase.java 1878394 2020-06-02 13:18:41Z ssteiner $ */ + +package org.apache.xmlgraphics.xmp; + +import java.io.StringReader; +import java.net.URL; +import java.util.Calendar; +import java.util.Date; +import java.util.TimeZone; + +import javax.xml.transform.TransformerException; +import javax.xml.transform.stream.StreamSource; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import org.apache.xmlgraphics.xmp.schemas.DublinCoreAdapter; +import org.apache.xmlgraphics.xmp.schemas.DublinCoreSchema; +import org.apache.xmlgraphics.xmp.schemas.XMPBasicAdapter; +import org.apache.xmlgraphics.xmp.schemas.XMPBasicSchema; +import org.apache.xmlgraphics.xmp.schemas.pdf.AdobePDFAdapter; +import org.apache.xmlgraphics.xmp.schemas.pdf.AdobePDFSchema; + +/** + * Tests for the XMP parser. + */ +public class XMPParserTestCase { + + @Test + public void testParseBasics() throws Exception { + URL url = getClass().getResource("test-basics.xmp"); + Metadata meta = XMPParser.parseXMP(url); + + DublinCoreAdapter dcAdapter = DublinCoreSchema.getAdapter(meta); + XMPBasicAdapter basicAdapter = XMPBasicSchema.getAdapter(meta); + AdobePDFAdapter pdfAdapter = AdobePDFSchema.getAdapter(meta); + + XMPProperty prop; + prop = meta.getProperty(XMPConstants.DUBLIN_CORE_NAMESPACE, "creator"); + XMPArray array; + array = prop.getArrayValue(); + assertEquals(1, array.getSize()); + assertEquals("John Doe", array.getValue(0).toString()); + assertEquals("John Doe", dcAdapter.getCreators()[0]); + + prop = meta.getProperty(XMPConstants.DUBLIN_CORE_NAMESPACE, "title"); + assertEquals("Example document", prop.getValue().toString()); + assertEquals("Example document", dcAdapter.getTitle()); + prop = meta.getProperty(XMPConstants.XMP_BASIC_NAMESPACE, "CreateDate"); + //System.out.println("Creation Date: " + prop.getValue() + " " + prop.getClass().getName()); + prop = meta.getProperty(XMPConstants.XMP_BASIC_NAMESPACE, "CreatorTool"); + assertEquals("An XML editor", prop.getValue().toString()); + assertEquals("An XML editor", basicAdapter.getCreatorTool()); + prop = meta.getProperty(XMPConstants.ADOBE_PDF_NAMESPACE, "Producer"); + assertEquals("Apache FOP Version SVN trunk", prop.getValue().toString()); + assertEquals("Apache FOP Version SVN trunk", pdfAdapter.getProducer()); + prop = meta.getProperty(XMPConstants.ADOBE_PDF_NAMESPACE, "PDFVersion"); + assertEquals("1.4", prop.getValue().toString()); + assertEquals("1.4", pdfAdapter.getPDFVersion()); + } + + @Test + public void testParse1() throws Exception { + URL url = getClass().getResource("unknown-schema.xmp"); + Metadata meta = XMPParser.parseXMP(url); + + DublinCoreAdapter dcAdapter = DublinCoreSchema.getAdapter(meta); + + XMPProperty prop; + //Access through the known schema as reference + prop = meta.getProperty(XMPConstants.DUBLIN_CORE_NAMESPACE, "title"); + assertEquals("Unknown Schema", prop.getValue().toString()); + assertEquals("Unknown Schema", dcAdapter.getTitle()); + + //Access through a schema unknown to the XMP framework + prop = meta.getProperty("http://unknown.org/something", "dummy"); + assertEquals("Dummy!", prop.getValue().toString()); + } + + @Test + public void testParseStructures() throws Exception { + URL url = getClass().getResource("test-structures.xmp"); + Metadata meta = XMPParser.parseXMP(url); + + XMPProperty prop; + + String testns = "http://foo.bar/test/"; + prop = meta.getProperty(testns, "something"); + assertEquals("blablah", prop.getValue().toString()); + + prop = meta.getProperty(testns, "ingredients"); + XMPArray array = prop.getArrayValue(); + assertEquals(3, array.getSize()); + XMPStructure struct = array.getStructure(0); + assertEquals(2, struct.getPropertyCount()); + prop = struct.getValueProperty(); + assertEquals("Apples", prop.getValue()); + prop = struct.getProperty(testns, "amount"); + assertEquals("4", prop.getValue()); + + prop = meta.getProperty(testns, "villain"); + XMPProperty prop1; + prop1 = prop.getStructureValue().getProperty(testns, "name"); + assertEquals("Darth Sidious", prop1.getValue()); + prop1 = prop.getStructureValue().getProperty(testns, "other-name"); + assertEquals("Palpatine", prop1.getValue()); + + //Test shorthand form + prop = meta.getProperty(testns, "project"); + prop1 = prop.getStructureValue().getProperty(testns, "name"); + assertEquals("Apache XML Graphics", prop1.getValue()); + prop1 = prop.getStructureValue().getProperty(testns, "url"); + assertEquals("http://xmlgraphics.apache.org/", prop1.getValue()); + + } + + @Test + public void testAttributeValues() throws Exception { + URL url = getClass().getResource("test-attribute-values.xmp"); + Metadata meta = XMPParser.parseXMP(url); + + DublinCoreAdapter dcAdapter = DublinCoreSchema.getAdapter(meta); + assertEquals("Ender's Game", dcAdapter.getTitle()); + assertEquals("Orson Scott Card", dcAdapter.getCreators()[0]); + } + + @Test + public void testParseDates() throws Exception { + URL url = getClass().getResource("test-dates.xmp"); + Metadata meta = XMPParser.parseXMP(url); + XMPProperty prop; + + DublinCoreAdapter dcAdapter = DublinCoreSchema.getAdapter(meta); + + //Simple adapter access + Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT+2:00")); + cal.set(2006, Calendar.JUNE, 2, 10, 36, 40); + cal.set(Calendar.MILLISECOND, 0); + assertEquals(cal.getTime(), dcAdapter.getDate()); + Date[] dates = dcAdapter.getDates(); + assertEquals(2, dates.length); + + //The second is the most recent and should match the simple value + assertEquals(dates[1], dcAdapter.getDate()); + + prop = meta.getProperty(XMPConstants.DUBLIN_CORE_NAMESPACE, "date"); + assertNotNull(prop.getArrayValue()); + assertEquals(2, prop.getArrayValue().getSize()); + + //Now add a new date and check if the adapter's getDate() method returns the new date. + cal.set(2008, Calendar.NOVEMBER, 1, 10, 10, 0); + dcAdapter.addDate(cal.getTime()); + assertEquals(3, dcAdapter.getDates().length); + prop = meta.getProperty(XMPConstants.DUBLIN_CORE_NAMESPACE, "date"); + assertNotNull(prop.getArrayValue()); + assertEquals(3, prop.getArrayValue().getSize()); + assertEquals(cal.getTime(), dcAdapter.getDate()); + } + + @Test + public void testParseEmptyValues() throws Exception { + URL url = getClass().getResource("empty-values.xmp"); + Metadata meta = XMPParser.parseXMP(url); + + DublinCoreAdapter dc = DublinCoreSchema.getAdapter(meta); + String title = dc.getTitle(); + assertEquals("empty", title); + + title = dc.getTitle("fr"); //Does not exist + assertNull(title); + + title = dc.getTitle("de"); + assertNull(title); //Empty value treated same as not existant + } + + @Test + public void testExternalDTD() { + String payload = "" + + "\n%remote;]>\n" + + ""; + StreamSource streamSource = new StreamSource(new StringReader(payload)); + String msg = ""; + try { + XMPParser.parseXMP(streamSource); + } catch (TransformerException e) { + msg = e.getMessage(); + } + assertTrue(msg, msg.contains("access is not allowed")); + } +} diff --git a/src/test/java/org/apache/xmlgraphics/xmp/XMPPropertyTestCase.java b/src/test/java/org/apache/xmlgraphics/xmp/XMPPropertyTestCase.java new file mode 100644 index 0000000..0aabff1 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/xmp/XMPPropertyTestCase.java @@ -0,0 +1,217 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: XMPPropertyTestCase.java 1829045 2018-04-13 09:22:33Z ssteiner $ */ + +package org.apache.xmlgraphics.xmp; + +import java.io.StringWriter; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Date; +import java.util.Locale; +import java.util.Set; +import java.util.TimeZone; + +import javax.xml.transform.stream.StreamResult; +import javax.xml.transform.stream.StreamSource; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import org.apache.xmlgraphics.util.QName; +import org.apache.xmlgraphics.xmp.schemas.DublinCoreAdapter; +import org.apache.xmlgraphics.xmp.schemas.DublinCoreSchema; +import org.apache.xmlgraphics.xmp.schemas.XMPBasicAdapter; +import org.apache.xmlgraphics.xmp.schemas.XMPBasicSchema; + +/** + * Tests property access methods. + */ +public class XMPPropertyTestCase { + + @Test + public void testPropertyAccess() throws Exception { + Metadata xmp = new Metadata(); + DublinCoreAdapter dc = DublinCoreSchema.getAdapter(xmp); + assertNull(dc.getContributors()); + + dc.addContributor("Contributor1"); + assertEquals(1, dc.getContributors().length); + assertEquals("Contributor1", dc.getContributors()[0]); + dc.removeContributor("Contributor1"); + assertNull(dc.getContributors()); + + dc.addContributor("Contributor1"); + assertEquals(1, dc.getContributors().length); + dc.addContributor("Contributor2"); + assertEquals(2, dc.getContributors().length); + assertFalse(dc.removeContributor("DoesNotExist")); + assertTrue(dc.removeContributor("Contributor1")); + assertEquals(1, dc.getContributors().length); + assertTrue(dc.removeContributor("Contributor2")); + assertFalse(dc.removeContributor("Contributor2")); + assertNull(dc.getContributors()); + } + + @Test + public void testPropertyRemovalLangAlt() throws Exception { + Metadata xmp = new Metadata(); + DublinCoreAdapter dc = DublinCoreSchema.getAdapter(xmp); + + //dc:title is a "Lang Alt" + dc.setTitle("en", "The title"); + String title = dc.removeTitle("en"); + assertEquals("The title", title); + dc.setTitle("en", "The title"); + dc.setTitle("de", "Der Titel"); + title = dc.removeTitle("en"); + assertEquals("The title", title); + title = dc.removeTitle("en"); + assertNull(title); + + title = dc.removeTitle("de"); + assertEquals("Der Titel", title); + title = dc.removeTitle("de"); + assertNull(title); + } + + @Test + public void testReplaceLangAlt() throws Exception { + Metadata xmp = new Metadata(); + DublinCoreAdapter dc = DublinCoreSchema.getAdapter(xmp); + dc.setTitle("Default title"); + StringWriter writer = new StringWriter(); + XMPSerializer.writeXML(xmp, new StreamResult(writer)); + String xmpString = writer.toString(); + xmp = XMPParser.parseXMP(new StreamSource(new java.io.StringReader(xmpString))); + dc = DublinCoreSchema.getAdapter(xmp); + assertEquals("Default title", dc.getTitle()); + dc.setTitle("Updated title"); + XMPProperty prop = xmp.getProperty(new QName(DublinCoreSchema.NAMESPACE, "title")); + XMPArray array = prop.getArrayValue(); + assertNotNull(array); + //Check that only one title is present. There used to be a bug that didn't set the + //non-qualified value equal to the value qualified with "x-default". + assertEquals(1, array.getSize()); + assertEquals("Updated title", array.getValue(0)); + } + + @Test + public void testPropertyValues() throws Exception { + Metadata xmp = new Metadata(); + DublinCoreAdapter dc = DublinCoreSchema.getAdapter(xmp); + + String format = dc.getFormat(); + assertNull(format); + + dc.setFormat("application/pdf"); + format = dc.getFormat(); + assertEquals("application/pdf", format); + + dc.setFormat("image/jpeg"); + format = dc.getFormat(); + assertEquals("image/jpeg", format); + + dc.setFormat(null); + format = dc.getFormat(); + assertNull(format); + + dc.setFormat(""); //Empty string same as null value + format = dc.getFormat(); + assertNull(format); + + dc.setTitle("title"); + String title = dc.getTitle(); + assertEquals("title", title); + + dc.setTitle("Titel"); + title = dc.getTitle(); + assertEquals("Titel", title); + + dc.setTitle(null); + title = dc.getTitle(); + assertNull(title); + + dc.setTitle(""); + title = dc.getTitle(); + assertNull(title); + } + + @Test + public void testDates() throws Exception { + Metadata xmp = new Metadata(); + XMPBasicAdapter basic = XMPBasicSchema.getAdapter(xmp); + + Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"), Locale.ENGLISH); + cal.set(2008, Calendar.FEBRUARY, 07, 15, 11, 07); + cal.set(Calendar.MILLISECOND, 0); + Date dt = cal.getTime(); + + assertNull(basic.getCreateDate()); + basic.setCreateDate(dt); + Date dt2 = basic.getCreateDate(); + assertEquals(dt2, dt); + } + + @Test + public void testQualifiers() throws Exception { + Metadata xmp = new Metadata(); + XMPBasicAdapter basic = XMPBasicSchema.getAdapter(xmp); + + basic.addIdentifier("x123"); + basic.setIdentifier("id1", "system1"); + basic.setIdentifier("12345", "system2"); + + String[] ids = basic.getIdentifiers(); + assertEquals(3, ids.length); + Set set = new java.util.HashSet(Arrays.asList(ids)); + assertTrue(set.contains("x123")); + assertTrue(set.contains("id1")); + assertTrue(set.contains("12345")); + + assertEquals("id1", basic.getIdentifier("system1")); + basic.setIdentifier("id2", "system1"); + assertEquals("id2", basic.getIdentifier("system1")); + assertEquals(3, basic.getIdentifiers().length); + } + + @Test + public void testEmptyPropertyValue() { + Metadata xmp = new Metadata(); + DublinCoreAdapter dc = DublinCoreSchema.getAdapter(xmp); + String ex = ""; + try { + dc.addLanguage(""); + } catch (IllegalArgumentException e) { + ex = e.getMessage(); + } + assertEquals(ex, "'language' value must not be empty"); + + try { + dc.addSubject(""); + } catch (IllegalArgumentException e) { + ex = e.getMessage(); + } + assertEquals(ex, "'subject' value must not be empty"); + } +} diff --git a/src/test/java/org/apache/xmlgraphics/xmp/empty-values.xmp b/src/test/java/org/apache/xmlgraphics/xmp/empty-values.xmp new file mode 100644 index 0000000..249aa49 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/xmp/empty-values.xmp @@ -0,0 +1,13 @@ + + + + + + + empty + + + + + + diff --git a/src/test/java/org/apache/xmlgraphics/xmp/test-attribute-values.xmp b/src/test/java/org/apache/xmlgraphics/xmp/test-attribute-values.xmp new file mode 100644 index 0000000..8835fd2 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/xmp/test-attribute-values.xmp @@ -0,0 +1,25 @@ + + + + + + + + diff --git a/src/test/java/org/apache/xmlgraphics/xmp/test-basics.xmp b/src/test/java/org/apache/xmlgraphics/xmp/test-basics.xmp new file mode 100644 index 0000000..c5ccc26 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/xmp/test-basics.xmp @@ -0,0 +1,39 @@ + + + + + + + + + John Doe + + + Example document + 2006-06-02T10:36:40+02:00 + + + 2006-06-02T10:36:40+02:00 + An XML editor + + + Apache FOP Version SVN trunk + 1.4 + + + diff --git a/src/test/java/org/apache/xmlgraphics/xmp/test-dates.xmp b/src/test/java/org/apache/xmlgraphics/xmp/test-dates.xmp new file mode 100644 index 0000000..4226f25 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/xmp/test-dates.xmp @@ -0,0 +1,30 @@ + + + + + + + + + 2006-04-27T23:01:02+02:00 + 2006-06-02T10:36:40+02:00 + + + + + diff --git a/src/test/java/org/apache/xmlgraphics/xmp/test-structures.xmp b/src/test/java/org/apache/xmlgraphics/xmp/test-structures.xmp new file mode 100644 index 0000000..9dca062 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/xmp/test-structures.xmp @@ -0,0 +1,61 @@ + + + + + + + 2007-12-23T16:22:04+01:00 + + + + blablah + + + + + Apples + 4 + + + + + Nuts + 12 + + + + + Hamburger + 1 + + + + + + + Darth Sidious + Palpatine + + + + Apache XML Graphics + http://xmlgraphics.apache.org/ + + + + diff --git a/src/test/java/org/apache/xmlgraphics/xmp/unknown-schema.xmp b/src/test/java/org/apache/xmlgraphics/xmp/unknown-schema.xmp new file mode 100644 index 0000000..647db53 --- /dev/null +++ b/src/test/java/org/apache/xmlgraphics/xmp/unknown-schema.xmp @@ -0,0 +1,28 @@ + + + + + + + Unknown Schema + + + Dummy! + + + diff --git a/src/test/resources/META-INF/MANIFEST.MF b/src/test/resources/META-INF/MANIFEST.MF new file mode 100644 index 0000000..c502781 --- /dev/null +++ b/src/test/resources/META-INF/MANIFEST.MF @@ -0,0 +1,4 @@ +Manifest-Version: 1.0 + +Name: sample.txt +Content-Type: text/plain diff --git a/src/test/resources/org/apache/xmlgraphics/io/test-catalog.xml b/src/test/resources/org/apache/xmlgraphics/io/test-catalog.xml new file mode 100644 index 0000000..8e28922 --- /dev/null +++ b/src/test/resources/org/apache/xmlgraphics/io/test-catalog.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/src/test/resources/org/apache/xmlgraphics/io/testResource.txt b/src/test/resources/org/apache/xmlgraphics/io/testResource.txt new file mode 100644 index 0000000..9cb459b --- /dev/null +++ b/src/test/resources/org/apache/xmlgraphics/io/testResource.txt @@ -0,0 +1 @@ +This is a text file used to test the CatalogResolver diff --git a/src/test/resources/org/apache/xmlgraphics/java2d/color/profile/ncp-example.icc b/src/test/resources/org/apache/xmlgraphics/java2d/color/profile/ncp-example.icc new file mode 100644 index 0000000..7afb2d8 Binary files /dev/null and b/src/test/resources/org/apache/xmlgraphics/java2d/color/profile/ncp-example.icc differ diff --git a/src/test/resources/org/apache/xmlgraphics/util/io/fourByte b/src/test/resources/org/apache/xmlgraphics/util/io/fourByte new file mode 100644 index 0000000..a6bddc4 --- /dev/null +++ b/src/test/resources/org/apache/xmlgraphics/util/io/fourByte @@ -0,0 +1 @@ +ABCD \ No newline at end of file diff --git a/src/test/resources/org/apache/xmlgraphics/util/io/fourByte.64 b/src/test/resources/org/apache/xmlgraphics/util/io/fourByte.64 new file mode 100644 index 0000000..100d04c --- /dev/null +++ b/src/test/resources/org/apache/xmlgraphics/util/io/fourByte.64 @@ -0,0 +1 @@ +QUJDRA== diff --git a/src/test/resources/org/apache/xmlgraphics/util/io/large b/src/test/resources/org/apache/xmlgraphics/util/io/large new file mode 100644 index 0000000..a3e28cc Binary files /dev/null and b/src/test/resources/org/apache/xmlgraphics/util/io/large differ diff --git a/src/test/resources/org/apache/xmlgraphics/util/io/medium b/src/test/resources/org/apache/xmlgraphics/util/io/medium new file mode 100644 index 0000000..d0a7ee7 Binary files /dev/null and b/src/test/resources/org/apache/xmlgraphics/util/io/medium differ diff --git a/src/test/resources/org/apache/xmlgraphics/util/io/medium.64 b/src/test/resources/org/apache/xmlgraphics/util/io/medium.64 new file mode 100644 index 0000000..6b0c5f6 --- /dev/null +++ b/src/test/resources/org/apache/xmlgraphics/util/io/medium.64 @@ -0,0 +1,23 @@ +H4sICFfuQDsAA21lZGl1bQCtVk1T4zgQvedXdOUyUGUCzO5c2I9ax1FAVcbOWjYstzG2Qlxj +WynZIZV/v6/lhG/2skMVRLFbr9973S1Bf/zEnxG9/0lXmvx1XuBDmWW/za2msCp022mPbrTt +KtPS+eR8RD+XyYgCs97Z6mHV01FwTF/Pzs4+JDM3m7bMe9CYkF/X5LZ0ZHWn7aMuJwyV6LLq +elvdbziO8rakTaepaqkzG1to9+S+anO7o6WxTefRtupXZKz7NJueGlNWy6rIT0bEGB5x7rW2 +TdX3uqS1NY9ViUW/ynv80cCpa7Ot2gcqTFtWvKlzmxrdXzCp88kbXh2ZJR0YFaZE6KbrCVL6 +HFQZNL83j/zu4Ay1pkcxPFe5flV1VAOQcV4mbcs3jJC1qPOq0dbZ8/U9E+R74cdAxGqoLDcg +9xGV/8uE9hJLU2wa3faupA4Nu05RCYO3lpq817bK6+7Zclcqt/WFBCfsl4lrGd2WJ6i3fY2N +hEW9eQVgX7ngUbVE8p3n5Dsq+y30RgXlxY/WbGtdPjD8BdE4ZQsGv/rDto6rO7StQyv1o67N +Ggzofucg/6O7iY5Wfb++OD3dbreT3MVNjH04PZ6MHZpfw5kW9tRg7Crwhha82+HZeq3zJ7cP +fKjqO10vPUhcHkynLQwHRUvdpmCDKluerHPb794Adyi+bfK63u3Rnfm/Dua3eQPh4ylE/Bg7 +WBp/rnJMQ6+hm+gefqBsaBnjKKGMxmJseSzhbGN6rPcWdzDTVo9s5RKv9vqf1B2mGOGVsQ5t +a3ly22GGu86dH3Me+HfPPVrXOu+42dE6xdAJQwH+eq6D0/xtQosDoSdGz4Se+HAlWCJrLGAc +ovamjD02kwNcmsPTQ9n2daus8xXlOihzwj4gTzyC2DG0yOdnJ7NPr6QiFc/TWz8RhPUiiW/k +TMzo+3df4cGXL+RHM/zekfhnkQil8C5OSF4vQokl9iV+lEqhcA7IKAizmYwuPZpmKUVxSqG8 +lini0thDMvHBPorndC2S4Apf/akMZXrHKUc0l2mEfERz5PNp4SepDLLQT4gWWbKIlSAmTTOp +gtCX12I2IVBAWiJxI6KU1JUfhsR5R+Qv/AD5n7QCNYtmfirjiJygVFEQR2kiQT1OkHYqwN6f +hsIRcBbMZCKC1CndL1kz/AL10CO1EIHkBbwSEOondx5jA1eJvzNE4S0I+9f+JYQfOb9wvXxu +GcoRZIm4ZjVslMqmKpVplgq6jOOZYnQlkhsZCPUbhbFSIw7LlPCQJvVdekaBmQjAl2mm5GCr +jFKRJNmCLTgmuopv4RrI+hnXmKseRyMnmx2MkztGZkdcjSDy9krgReJMd9b5bAgpeBikryKR +F56mo71k1ksUictQXoooEI5kzFi3UoljlFUqDgGuy3/rIzfcQX7uIZaH9Zvu9VzRSc7Jn91I +16ZDODpFyX1jOQuDq30JhhF4NaY88R1uBHczP5p6g/nHlcjnwPNV2eR8IeAAb3BXYD7LCtfS +BjfUiHj87vUqr5eHMXw3gq/+f+HzpzH8tGr53h0OftMOA/z59D6dUJ3W9PvH18Sfk9G/1zkl +kS0KAAA= diff --git a/src/test/resources/org/apache/xmlgraphics/util/io/medium.pc.64 b/src/test/resources/org/apache/xmlgraphics/util/io/medium.pc.64 new file mode 100644 index 0000000..6b0c5f6 --- /dev/null +++ b/src/test/resources/org/apache/xmlgraphics/util/io/medium.pc.64 @@ -0,0 +1,23 @@ +H4sICFfuQDsAA21lZGl1bQCtVk1T4zgQvedXdOUyUGUCzO5c2I9ax1FAVcbOWjYstzG2Qlxj +WynZIZV/v6/lhG/2skMVRLFbr9973S1Bf/zEnxG9/0lXmvx1XuBDmWW/za2msCp022mPbrTt +KtPS+eR8RD+XyYgCs97Z6mHV01FwTF/Pzs4+JDM3m7bMe9CYkF/X5LZ0ZHWn7aMuJwyV6LLq +elvdbziO8rakTaepaqkzG1to9+S+anO7o6WxTefRtupXZKz7NJueGlNWy6rIT0bEGB5x7rW2 +TdX3uqS1NY9ViUW/ynv80cCpa7Ot2gcqTFtWvKlzmxrdXzCp88kbXh2ZJR0YFaZE6KbrCVL6 +HFQZNL83j/zu4Ay1pkcxPFe5flV1VAOQcV4mbcs3jJC1qPOq0dbZ8/U9E+R74cdAxGqoLDcg +9xGV/8uE9hJLU2wa3faupA4Nu05RCYO3lpq817bK6+7Zclcqt/WFBCfsl4lrGd2WJ6i3fY2N +hEW9eQVgX7ngUbVE8p3n5Dsq+y30RgXlxY/WbGtdPjD8BdE4ZQsGv/rDto6rO7StQyv1o67N +Ggzofucg/6O7iY5Wfb++OD3dbreT3MVNjH04PZ6MHZpfw5kW9tRg7Crwhha82+HZeq3zJ7cP +fKjqO10vPUhcHkynLQwHRUvdpmCDKluerHPb794Adyi+bfK63u3Rnfm/Dua3eQPh4ylE/Bg7 +WBp/rnJMQ6+hm+gefqBsaBnjKKGMxmJseSzhbGN6rPcWdzDTVo9s5RKv9vqf1B2mGOGVsQ5t +a3ly22GGu86dH3Me+HfPPVrXOu+42dE6xdAJQwH+eq6D0/xtQosDoSdGz4Se+HAlWCJrLGAc +ovamjD02kwNcmsPTQ9n2daus8xXlOihzwj4gTzyC2DG0yOdnJ7NPr6QiFc/TWz8RhPUiiW/k +TMzo+3df4cGXL+RHM/zekfhnkQil8C5OSF4vQokl9iV+lEqhcA7IKAizmYwuPZpmKUVxSqG8 +lini0thDMvHBPorndC2S4Apf/akMZXrHKUc0l2mEfERz5PNp4SepDLLQT4gWWbKIlSAmTTOp +gtCX12I2IVBAWiJxI6KU1JUfhsR5R+Qv/AD5n7QCNYtmfirjiJygVFEQR2kiQT1OkHYqwN6f +hsIRcBbMZCKC1CndL1kz/AL10CO1EIHkBbwSEOondx5jA1eJvzNE4S0I+9f+JYQfOb9wvXxu +GcoRZIm4ZjVslMqmKpVplgq6jOOZYnQlkhsZCPUbhbFSIw7LlPCQJvVdekaBmQjAl2mm5GCr +jFKRJNmCLTgmuopv4RrI+hnXmKseRyMnmx2MkztGZkdcjSDy9krgReJMd9b5bAgpeBikryKR +F56mo71k1ksUictQXoooEI5kzFi3UoljlFUqDgGuy3/rIzfcQX7uIZaH9Zvu9VzRSc7Jn91I +16ZDODpFyX1jOQuDq30JhhF4NaY88R1uBHczP5p6g/nHlcjnwPNV2eR8IeAAb3BXYD7LCtfS +BjfUiHj87vUqr5eHMXw3gq/+f+HzpzH8tGr53h0OftMOA/z59D6dUJ3W9PvH18Sfk9G/1zkl +kS0KAAA= diff --git a/src/test/resources/org/apache/xmlgraphics/util/io/oneByte b/src/test/resources/org/apache/xmlgraphics/util/io/oneByte new file mode 100644 index 0000000..8c7e5a6 --- /dev/null +++ b/src/test/resources/org/apache/xmlgraphics/util/io/oneByte @@ -0,0 +1 @@ +A \ No newline at end of file diff --git a/src/test/resources/org/apache/xmlgraphics/util/io/oneByte.64 b/src/test/resources/org/apache/xmlgraphics/util/io/oneByte.64 new file mode 100644 index 0000000..d507d4f --- /dev/null +++ b/src/test/resources/org/apache/xmlgraphics/util/io/oneByte.64 @@ -0,0 +1 @@ +QQ== diff --git a/src/test/resources/org/apache/xmlgraphics/util/io/small b/src/test/resources/org/apache/xmlgraphics/util/io/small new file mode 100644 index 0000000..d22228d --- /dev/null +++ b/src/test/resources/org/apache/xmlgraphics/util/io/small @@ -0,0 +1,4 @@ + Thomas DeWeese +Thomas.DeWeese@Kodak.com + "The only difference between theory and practice is + that in theory there isn't any." -- unknown diff --git a/src/test/resources/org/apache/xmlgraphics/util/io/small.64 b/src/test/resources/org/apache/xmlgraphics/util/io/small.64 new file mode 100644 index 0000000..fa757b0 --- /dev/null +++ b/src/test/resources/org/apache/xmlgraphics/util/io/small.64 @@ -0,0 +1,3 @@ +CQkJCQkJCVRob21hcyBEZVdlZXNlClRob21hcy5EZVdlZXNlQEtvZGFrLmNvbQoJCQkgICJU +aGUgb25seSBkaWZmZXJlbmNlIGJldHdlZW4gdGhlb3J5IGFuZCBwcmFjdGljZSBpcwoJCQkg +ICB0aGF0IGluIHRoZW9yeSB0aGVyZSBpc24ndCBhbnkuIiAtLSB1bmtub3duCg== diff --git a/src/test/resources/org/apache/xmlgraphics/util/io/tenByte b/src/test/resources/org/apache/xmlgraphics/util/io/tenByte new file mode 100644 index 0000000..5080abd --- /dev/null +++ b/src/test/resources/org/apache/xmlgraphics/util/io/tenByte @@ -0,0 +1 @@ +JIHGFEDCBA \ No newline at end of file diff --git a/src/test/resources/org/apache/xmlgraphics/util/io/tenByte.64 b/src/test/resources/org/apache/xmlgraphics/util/io/tenByte.64 new file mode 100644 index 0000000..2fb3891 --- /dev/null +++ b/src/test/resources/org/apache/xmlgraphics/util/io/tenByte.64 @@ -0,0 +1 @@ +SklIR0ZFRENCQQ== diff --git a/src/test/resources/org/apache/xmlgraphics/util/io/threeByte b/src/test/resources/org/apache/xmlgraphics/util/io/threeByte new file mode 100644 index 0000000..48b83b8 --- /dev/null +++ b/src/test/resources/org/apache/xmlgraphics/util/io/threeByte @@ -0,0 +1 @@ +ABC \ No newline at end of file diff --git a/src/test/resources/org/apache/xmlgraphics/util/io/threeByte.64 b/src/test/resources/org/apache/xmlgraphics/util/io/threeByte.64 new file mode 100644 index 0000000..869bc61 --- /dev/null +++ b/src/test/resources/org/apache/xmlgraphics/util/io/threeByte.64 @@ -0,0 +1 @@ +QUJD diff --git a/src/test/resources/org/apache/xmlgraphics/util/io/twoByte b/src/test/resources/org/apache/xmlgraphics/util/io/twoByte new file mode 100644 index 0000000..dfc9179 --- /dev/null +++ b/src/test/resources/org/apache/xmlgraphics/util/io/twoByte @@ -0,0 +1 @@ +AB \ No newline at end of file diff --git a/src/test/resources/org/apache/xmlgraphics/util/io/twoByte.64 b/src/test/resources/org/apache/xmlgraphics/util/io/twoByte.64 new file mode 100644 index 0000000..483d7af --- /dev/null +++ b/src/test/resources/org/apache/xmlgraphics/util/io/twoByte.64 @@ -0,0 +1 @@ +QUI= diff --git a/src/test/resources/org/apache/xmlgraphics/util/io/zeroByte b/src/test/resources/org/apache/xmlgraphics/util/io/zeroByte new file mode 100644 index 0000000..e69de29 diff --git a/src/test/resources/org/apache/xmlgraphics/util/io/zeroByte.64 b/src/test/resources/org/apache/xmlgraphics/util/io/zeroByte.64 new file mode 100644 index 0000000..e69de29 diff --git a/src/test/resources/sample.txt b/src/test/resources/sample.txt new file mode 100644 index 0000000..c2b5e6a --- /dev/null +++ b/src/test/resources/sample.txt @@ -0,0 +1 @@ +A sample text diff --git a/src/tools/resources/checkstyle/LICENSE.txt b/src/tools/resources/checkstyle/LICENSE.txt new file mode 100644 index 0000000..b916c3e --- /dev/null +++ b/src/tools/resources/checkstyle/LICENSE.txt @@ -0,0 +1,14 @@ +Licensed to the Apache Software Foundation (ASF) under one or more +contributor license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright ownership. +The ASF licenses this file to You under the Apache License, Version 2.0 +(the "License"); you may not use this file except in compliance with +the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/src/tools/resources/checkstyle/checkstyle.header b/src/tools/resources/checkstyle/checkstyle.header new file mode 100644 index 0000000..d49f166 --- /dev/null +++ b/src/tools/resources/checkstyle/checkstyle.header @@ -0,0 +1,20 @@ +^\/\* +^ \* Licensed to the Apache Software Foundation \(ASF\) under one or more +^ \* contributor license agreements. See the NOTICE file distributed with +^ \* this work for additional information regarding copyright ownership. +^ \* The ASF licenses this file to You under the Apache License, Version 2.0 +^ \* \(the "License"\); you may not use this file except in compliance with +^ \* the License. You may obtain a copy of the License at +^ \* +^ \* http://www.apache.org/licenses/LICENSE-2.0 +^ \* +^ \* Unless required by applicable law or agreed to in writing, software +^ \* distributed under the License is distributed on an "AS IS" BASIS, +^ \* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +^ \* See the License for the specific language governing permissions and +^ \* limitations under the License. +^ \*\/ + +^\/\* \$Id.*\$ \*\/ + +^package .* \ No newline at end of file diff --git a/src/tools/resources/checkstyle/checkstyle.xml b/src/tools/resources/checkstyle/checkstyle.xml new file mode 100644 index 0000000..bff3fd8 --- /dev/null +++ b/src/tools/resources/checkstyle/checkstyle.xml @@ -0,0 +1,269 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/tools/resources/checkstyle/suppressions.xml b/src/tools/resources/checkstyle/suppressions.xml new file mode 100644 index 0000000..650fb8d --- /dev/null +++ b/src/tools/resources/checkstyle/suppressions.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/tools/resources/findbugs/exclusions.xml b/src/tools/resources/findbugs/exclusions.xml new file mode 100644 index 0000000..f5f2d49 --- /dev/null +++ b/src/tools/resources/findbugs/exclusions.xml @@ -0,0 +1,192 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/images/asf-logo.png b/test/images/asf-logo.png new file mode 100644 index 0000000..b831a2d Binary files /dev/null and b/test/images/asf-logo.png differ diff --git a/test/images/barcode.eps b/test/images/barcode.eps new file mode 100644 index 0000000..650c0c3 --- /dev/null +++ b/test/images/barcode.eps @@ -0,0 +1,79 @@ +%!PS-Adobe-3.0 EPSF-3.0 +%%BoundingBox: 0 0 136 43 +%%HiResBoundingBox: 0 0 135.6548 42.525 +%%Creator: Barcode4J (http://barcode4j.krysalis.org) +%%CreationDate: 2005-08-15T10:58:35 +%%LanguageLevel: 1 +%%EndComments +%%BeginProlog +%%BeginProcSet: barcode4j-procset 1.0 +/rf { +newpath +4 -2 roll moveto +dup neg 0 exch rlineto +exch 0 rlineto +0 neg exch rlineto +closepath fill +} def +/ct { +moveto dup stringwidth +2 div neg exch 2 div neg exch +rmoveto show +} def +/jt { +4 -1 roll dup stringwidth pop +5 -2 roll 1 index sub +3 -1 roll sub +2 index length +1 sub div +0 4 -1 roll 4 -1 roll 5 -1 roll +moveto ashow +} def +%%EndProcSet: barcode4j-procset 1.0 +%%EndProlog +9.3555 42.525 0.9356 38.525 rf +11.2266 42.525 0.9356 38.525 rf +14.0332 42.525 1.8711 34.525 rf +17.7755 42.525 0.9356 34.525 rf +20.5821 42.525 0.9356 34.525 rf +22.4532 42.525 2.8066 34.525 rf +26.1954 42.525 0.9356 34.525 rf +29.9376 42.525 1.8711 34.525 rf +32.7442 42.525 1.8711 34.525 rf +37.422 42.525 0.9356 34.525 rf +41.1642 42.525 0.9356 34.525 rf +43.9709 42.525 0.9356 34.525 rf +48.6486 42.525 0.9356 34.525 rf +50.5197 42.525 0.9356 34.525 rf +/Helvetica findfont 7.999999999999999 scalefont setfont +(4) 3.2744 0.5644 ct +/Helvetica findfont 7.999999999999999 scalefont setfont +(194586) 13.0977 50.5197 0.5644 jt +52.3908 42.525 0.9356 38.525 rf +54.2619 42.525 0.9356 38.525 rf +56.133 42.525 0.9356 34.525 rf +59.8752 42.525 0.9356 34.525 rf +62.6818 42.525 2.8066 34.525 rf +67.3596 42.525 0.9356 34.525 rf +69.2307 42.525 0.9356 34.525 rf +72.0373 42.525 2.8066 34.525 rf +75.7795 42.525 0.9356 34.525 rf +78.5862 42.525 2.8066 34.525 rf +82.3284 42.525 2.8066 34.525 rf +87.0061 42.525 0.9356 34.525 rf +88.8772 42.525 0.9356 34.525 rf +90.7483 42.525 0.9356 34.525 rf +/Helvetica findfont 7.999999999999999 scalefont setfont +(705506) 57.0685 94.4905 0.5644 jt +95.4261 42.525 0.9356 38.525 rf +97.2972 42.525 0.9356 38.525 rf +107.5882 34.525 0.9356 30.525 rf +109.4593 34.525 1.8711 30.525 rf +114.1371 34.525 1.8711 30.525 rf +116.9437 34.525 0.9356 30.525 rf +118.8148 34.525 0.9356 30.525 rf +120.6859 34.525 0.9356 30.525 rf +124.4281 34.525 1.8711 30.525 rf +/Helvetica findfont 7.999999999999999 scalefont setfont +(04) 116.9437 35.0894 ct +%%EOF diff --git a/test/images/basi2c08.png b/test/images/basi2c08.png new file mode 100644 index 0000000..2aab44d Binary files /dev/null and b/test/images/basi2c08.png differ diff --git a/test/images/basn0g08.png b/test/images/basn0g08.png new file mode 100644 index 0000000..23c8237 Binary files /dev/null and b/test/images/basn0g08.png differ diff --git a/test/images/basn2c08.png b/test/images/basn2c08.png new file mode 100644 index 0000000..db5ad15 Binary files /dev/null and b/test/images/basn2c08.png differ diff --git a/test/images/basn3p08.png b/test/images/basn3p08.png new file mode 100644 index 0000000..0ddad07 Binary files /dev/null and b/test/images/basn3p08.png differ diff --git a/test/images/basn4a08.png b/test/images/basn4a08.png new file mode 100644 index 0000000..3e13052 Binary files /dev/null and b/test/images/basn4a08.png differ diff --git a/test/images/basn6a08.png b/test/images/basn6a08.png new file mode 100644 index 0000000..e608738 Binary files /dev/null and b/test/images/basn6a08.png differ diff --git a/test/images/bgimg300dpi.bmp b/test/images/bgimg300dpi.bmp new file mode 100644 index 0000000..afc407e Binary files /dev/null and b/test/images/bgimg300dpi.bmp differ diff --git a/test/images/bgimg300dpi.jpg b/test/images/bgimg300dpi.jpg new file mode 100644 index 0000000..a2ce7a3 Binary files /dev/null and b/test/images/bgimg300dpi.jpg differ diff --git a/test/images/bgimg72dpi.gif b/test/images/bgimg72dpi.gif new file mode 100644 index 0000000..5ad119b Binary files /dev/null and b/test/images/bgimg72dpi.gif differ diff --git a/test/images/big-image.png b/test/images/big-image.png new file mode 100644 index 0000000..e08b02c Binary files /dev/null and b/test/images/big-image.png differ diff --git a/test/images/cmyk-pxcm.jpg b/test/images/cmyk-pxcm.jpg new file mode 100644 index 0000000..2227613 Binary files /dev/null and b/test/images/cmyk-pxcm.jpg differ diff --git a/test/images/cmyk.jpg b/test/images/cmyk.jpg new file mode 100644 index 0000000..cfdcca1 Binary files /dev/null and b/test/images/cmyk.jpg differ diff --git a/test/images/corrupt-icc.png b/test/images/corrupt-icc.png new file mode 100644 index 0000000..7cd490f Binary files /dev/null and b/test/images/corrupt-icc.png differ diff --git a/test/images/corrupt-image.png b/test/images/corrupt-image.png new file mode 100644 index 0000000..b25c954 Binary files /dev/null and b/test/images/corrupt-image.png differ diff --git a/test/images/dirOnly.tif b/test/images/dirOnly.tif new file mode 100644 index 0000000..b9600a1 Binary files /dev/null and b/test/images/dirOnly.tif differ diff --git a/test/images/f00n2c08.png b/test/images/f00n2c08.png new file mode 100644 index 0000000..d6a1fff Binary files /dev/null and b/test/images/f00n2c08.png differ diff --git a/test/images/f04n2c08.png b/test/images/f04n2c08.png new file mode 100644 index 0000000..3c8b511 Binary files /dev/null and b/test/images/f04n2c08.png differ diff --git a/test/images/iccTest.jpg b/test/images/iccTest.jpg new file mode 100644 index 0000000..f676d50 Binary files /dev/null and b/test/images/iccTest.jpg differ diff --git a/test/images/iccTest.png b/test/images/iccTest.png new file mode 100644 index 0000000..c670369 Binary files /dev/null and b/test/images/iccTest.png differ diff --git a/test/images/img-with-tiff-preview.eps b/test/images/img-with-tiff-preview.eps new file mode 100644 index 0000000..20f494b Binary files /dev/null and b/test/images/img-with-tiff-preview.eps differ diff --git a/test/images/img.emf b/test/images/img.emf new file mode 100644 index 0000000..f8534da Binary files /dev/null and b/test/images/img.emf differ diff --git a/test/images/no-resolution.bmp b/test/images/no-resolution.bmp new file mode 100644 index 0000000..a94701f Binary files /dev/null and b/test/images/no-resolution.bmp differ diff --git a/test/images/no-resolution.png b/test/images/no-resolution.png new file mode 100644 index 0000000..0894f0d Binary files /dev/null and b/test/images/no-resolution.png differ diff --git a/test/images/no-resolution.tif b/test/images/no-resolution.tif new file mode 100644 index 0000000..18e778b Binary files /dev/null and b/test/images/no-resolution.tif differ diff --git a/test/images/pp0n6a08.png b/test/images/pp0n6a08.png new file mode 100644 index 0000000..4ed7a30 Binary files /dev/null and b/test/images/pp0n6a08.png differ diff --git a/test/images/tbbn3p08.png b/test/images/tbbn3p08.png new file mode 100644 index 0000000..0ede357 Binary files /dev/null and b/test/images/tbbn3p08.png differ diff --git a/test/images/tbrn2c08.png b/test/images/tbrn2c08.png new file mode 100644 index 0000000..5cca0d6 Binary files /dev/null and b/test/images/tbrn2c08.png differ diff --git a/test/images/tiff_group4.tif b/test/images/tiff_group4.tif new file mode 100644 index 0000000..5720e8f Binary files /dev/null and b/test/images/tiff_group4.tif differ diff --git a/xmlgraphics-commons-pom-template.pom b/xmlgraphics-commons-pom-template.pom new file mode 100644 index 0000000..84663bb --- /dev/null +++ b/xmlgraphics-commons-pom-template.pom @@ -0,0 +1,81 @@ + + + + + 4.0.0 + org.apache.xmlgraphics + xmlgraphics-commons + jar + Apache XML Graphics Commons + @version@ + http://xmlgraphics.apache.org/commons/ + + Apache XML Graphics Commons is a library that consists of several reusable + components used by Apache Batik and Apache FOP. Many of these components + can easily be used separately outside the domains of SVG and XSL-FO. + + 2005 + + + XML Graphics General List + general-subscribe@xmlgraphics.apache.org + general-unsubscribe@xmlgraphics.apache.org + http://mail-archives.apache.org/mod_mbox/xmlgraphics-general/ + + + XML Graphics Commit List + commits-subscribe@xmlgraphics.apache.org + commits-unsubscribe@xmlgraphics.apache.org + http://mail-archives.apache.org/mod_mbox/xmlgraphics-commits/ + + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + scm:svn:http://svn.apache.org/repos/asf/xmlgraphics/commons/trunk + scm:svn:https://svn.apache.org/repos/asf/xmlgraphics/commons/trunk + http://svn.apache.org/viewvc/xmlgraphics/commons/trunk/?root=Apache-SVN + + + Apache Software Foundation + http://www.apache.org/ + + + org.apache + apache + 7 + + + + commons-io + commons-io + 1.3.1 + + + commons-logging + commons-logging + 1.0.4 + + +